1. Grundlegende Konzepte - oth

Werbung
Die Programmiersprache C++
Prof. Jürgen Sauer
Programmieren in C++
Skriptum zur Vorlesung im SS 2003
1
Die Programmiersprache C++
Inhaltsverzeichnis
1.
Grundlegende Konzepte
1.1
Übersicht zur Entwicklung der Programmierspache C++
1.2
Ein einführendes Beispiel zur Präsentation grundlegender Konzepte
1.2.1 Aufgabenstellung und Lösungsvorschlag zu einem internen Sortierverfahren
1.2.2 Der Aufbau eines C++-Programms
1.2.2.1 Das Layout
1.2.2.2 Übersetzung
1.2.2.3 Entwicklungszklus eines C++-Programms
1.2.2.4 Kommentare und Präprozessor-Direktiven
1.2.2.5 Standardein-/Standardausgabe
1.2.2.6 Hauptprogramm
1.2.2.7 Kommandozeilenversionen bzw. Konsolenanwendungen von Visual C++ 6.0 bzw.
Dev-C++
1. Kommandozeilenversion von Visual C++
2. Konsolenanwendung in IDE unter Visual C++
3. Dev-C++
1.2.2.8 Visual C++ .NET
1. Erstellen einer prozedurorientierten C/C++-Konsolenanwendung
2. Erstellen einer prozedurorientierten C/C++-Windows-Anwendung
3. Verwaltete C++-Anwendung
4. Verwaltete C++-Anwendung mit GDI+
5.
1.3
Deklaration und Definition von Bezeichnern
1.4
1.4.1
1.4.2
1.4.3
1.4.4
Speicherklassen, Gültigkeitsbereiche und Namensbereiche
Speicherklassen
Gültigkeitsbereiche bzw. Geltungsbereiche
Externe Variable und Funktionen
Namensbereiche
1.5
1.5.1
1.5.2
1.5.3
1.5.4
1.5.5
1.5.6
1.5.7
1.5.8
Ausdrücke
Arithmetische Operatoren
Relationale und logische Operatoren
Bitweise logische Operatoren
Zuweisungsoperatoren
Inkrement- und Dekrementoperatoren
Bedungungsoperator
Kommaoperator
Vorrang und Assoziativität von Operatoren
1.6
1.6.1
1.6.2
Anweisungen
Einfache Anweisung
Kontrollanweisungen (-strukturen)
1. Die for-Anweisung
2
Die Programmiersprache C++
2. die while-Anweisung
3. die do-Anweisung
4. continue und break
5. die if-Anweisung
6. die „switch“-Anweisung
1.7
Funktionen
1.7.1 Definition und Deklaration von Funktionen
1.7.2 Parameterübergabe
1. Call by value
2. Call by reference
3. Vektoren als Parameter
4. Default-Argumente
5. Beliebig viele Argumente (variable Parameterlisten)
6. Prototypen
1.7.3 Funktionswerte (Ergebniswertrückgabe)
1.7.4 Rekursion
1.7.4.1 Rekursive Funktionen
1.7.4.2 Rekursion und Iteration
1.7.4.3 Türme von Hanoi
1.7.4.4 Damen-Problem
1.7.5 Überladen von Funktionsnamen (overloading)
1.7.6 Operatorfunktionen
1.7.7 Inline-Funktionen
1.7.8 Funktionsschablonen
1.7.9 Objekte als Funktionen
1.7.10 Spezifikation von Funktionen
2.
Datentypen
2.1
Einfache, fundamentale Datentypen
2.2
Abgeleitete Datentypen
2.2.1 Konstanten
2.2.2 Zeiger (pointer types)
2.2.3 Vektoren (Arrays)
2.2.3.1 C-Arrays
2.2.3.2 Der C++-Standardtyp vector
2.2.3.3 Der C++-Stringklasse string
2.2.4 Strukturen
2.2.5 Variantenstrukturen
3.
Benutzerdefinierte Datentypen: Klassen
3.1
3.1.1
3.1.2
3.1.3
Konzepte für benutzerdefinierte Datentypen
Formale Definitionsmöglichkeiten
Ein einführendes Beispiel: Stapelverarbeitung
Konstruktoren, Klassenvariable und Destruktoren
3
Die Programmiersprache C++
3.1.4
3.1.5
3.1.6
3.1.7
3.1.8
Operatorfunktionen
Konstante Komponentenfunktionen
Statische Komponentenfunktionen
„friend“-Funktionen und „friend“-Klassen
ADT-Stapel
3.2
3.2.1
3.2.2
3.2.3
3.2.4
3.2.5
3.2.6
3.2.7
3.2.8
Abgeleitete Klassen
Basisklasse und Ableitung
Einfache Verarbeitung
Klassenhierarchien
1. Konstruktoren in Klassenhierarchien
2. Destruktoren in Klassenhierarchien
Virtuelle Funktionen
Abstrakte Klassen
Mehrfachvererbung
Virtuelle Basisklassen
Generische Datentypen
3.3
3.3.1
3.3.2
Schablonen (Templates)
Klassenschablonen
Methodenschablonen
3.4
3.4.1
3.4.2
Ein-, Ausgabe
Aufbau
Ausgabe
1. Formatierte Ausgabe
2. Unformatierte Ausgabe
3. Adressierung von Streampositionen
4. Ausgabe auf Dateien
3.4.3 Eingabe
1. Formatierte Eingabe
2. unformatierte Eingabe
3. Eingabe aus Dateien
4. Eingabe aus Zeichenketten
3.4.4 Formatierung in Zeichenketten
3.4.4.1 strstream für C++ im AT&T-Standard
3.4.4.2 stringstream für C++ im ANSI/ISO-Standard
3.4.5
3.4.6
3.5
3.5.1
3.5.2
3.5.3
3.5.4
3.5.5
Fehlerzustände
Positionieren in Dateien
Ausnahmebehandlung
Übliche Fehlerbehandlungsroutinen
Schema für Ausnahmenbehandlungen
Exception-Hierarchie
Besondere Fehlerbehandlungsroutinen
Unbehandelte Ausnahmen
4
Die Programmiersprache C++
4.
Templates für Algorithmen und Datenstrukturen
4.1
Darstellung von Algorithmen und Datenstrukturen für Graphen
4.1.1 Die Datentruktur Graph
4.1.2 Die STl-Containerklasse vector zur Implementierung einer Knotenliste für
4.1.3 Mehrdimensionale Felder
4.1.4 Durchlaufen von Graphen mit Hilfe der Containerklassen stack und queue
4.1.4.1 Tiefensuche
4.1.4.2 Breitensuche
4.1.5 Ermittlung der kürzesten Wege mit Hilfe der STL-Containerklasse priority_queue
4.2
4.2.1
4.2.2
Verkettet gespeicherte Listen
Doppelt gekettete Listen
Ringförmig geschlossene Listen
4.3
4.4.1
4.4.2
4.4.3
Tabellen
Einfache Tabellen
Sortierte Tabellen
Hash-Tabellen
4.4
Binäre Bäume
5.
C und C++-Bibliotheken
5.1
Die C++-Standardbibliothek
5.1.1 Die C++-Standardbibliothek und die STL
5.1.2 Hilfsfunktionen und -klassen
5.1.2.1 Paare
5.1.2.2 Funktionsobjekte
5.2
5.2.1
5.2.2
5.2.3
5.2.4
5.2.5
5.2.6
5.2.7
5.2.8
Container
Bitset
Deque
List
Map
Queue
Set
Stack
Vector
5.3
5.3.1
5.3.2
5.3.3
5.3.4
5.3.5
Iteratoren
Iteratorkategorien
distance(), advance() und iter_swap()
Iterator-Adapter
Stream-Iteratoren
Stream-Traits
5.4
Algorithmen
5
Die Programmiersprache C++
5.5
Nationale Besonderheiten
5.6
5.6.1
5.6.2
5.6.3
5.6.2
5.6.2.1
5.6.2.2
5.6.2.3
5.6.2.4
5.6.2.5
5.6.2.6
5.6.2.7
5.6.2.8
5.6.2.9
Numerik
Komplexe Zahlen
Grenzwerte von Zahlensystemen
Halbnumerische Algorithmen
Optimierte numerische Algorithmen (valarry)
Konstruktoren und Elementfunktionen
Binäre Valarray-Operatoren
Mathematische Funktionen
slice
slice_array
gslice
gslice_array
mask_array
indirect_array
5.7
Typerkennung zur Laufzeit
5.8
5.8.1
5.8.2
Speichermanagement
<new>
<memory>
6.
Windows-Programmierung unter Visual C++
6.1
6.1.1
6.1.2
6.1.3
6.1.4
6.1.5
Merkmale von Visual C++
Visual C++-Features
Funktionsweise von Windows-Programmen
Eintritt in die Nachrichtenverarbeitung
Vom API zur MFC
Erstellen einer Windows-Anwendung mit der MFC
6.2
6.2.1
6.2.2
6.2.3
6.2.3.1
6.2.3.2
6.2.3.3
Die zentralen MFC-Klassen (Zusammensetzung und Zusammenspiel)
Übersicht zu den zentralen MFC-Klassen
Allgemeine Ereignisbehandlung unter Windows mit der MFC
Verschiedene spezielle MFC-Klassen
Spezielle Klassen zur Textverarbeitung
Dateiverarbeitung
CTimer
6.3
6.3.1
6.3.2
Die Kernkomponenten
Aufbau von Dialogen aus Steuerelementen
Die wichtigsten Steuerelemente
6.4
6.4.1
6.4.2
6.4.3
6.4.4
Erstellen von Dialogen über Ressourcen
Ressorcen
Dialoge aus Ressourcen
Visuelle Erstellung
Interaktive Platzierung der Komponenten
6
Die Programmiersprache C++
6.5
6.5.1
6.5.2
6.5.3
6.5.3.1
6.5.3.2
6.5.4
6.5.4.1
6.5.4.2
6.5.4.3
6.5.4.4
6.5.4.5
Assistenten und MFC-Anwendungen
Assistenten
Doc/View-Modell
SDI- ubd MDI-Anwendungen
SDI-Anwendungen
MDI-Anwendungen
Das MFC-Anwendungsgerüst
Erzeugen der Fenster
Anpassen der Fenster
Bearbeitung von Kommandozeilenargumenten
Die Nachricht WM_PAINT
Zeitgeber
6.6
6.8.1
6.8.2
Bilder, Zeichnungen und Bitmaps
Die grafische Geräteschnitsstelle
Die Zeichenwerkzeuge
6.7
Sammlungsklassen der MFC
7.
C# und .NET
8.
Die grafischen Bedienoberflächen X und OSF/Motif
8.1
X-Window bzw. X
8.1.1
8.1.2
8.1.3
Die Komponenten von X
Architektur von X-Programmen
Ein X-Programm
8.2
OSF/Motif
8.2.1
8.2.2
8.2.3
Einführung in OSF/Motif, Xt-Intrinsics
Struktur eines Intrinsics-Programms
Das OSF/Widget Set
7
Die Programmiersprache C++
1. Grundlegende Konzepte
1.1 Übersicht zur Entwicklung der Sprache C++
C++ ist eine relativ junge Sprache. Erste Versionen dieser Sprache wurden unter
dem Namen „C with Classes“1 1980 erstmals benutzt. Diese Erweiterung der
Programmiersprache C war zur Entwicklung von Simulationsprogrammen nötig. So
entstand auf der Basis von C unter Berücksichtigung von Konzepten, die in der
Simulationssprache Simula entwickelt wurden, eine objektorientierte Erweiterung zu
C. Je länger „C with Classes“ und später C++2 verwendet wurde, desto größer wurde
der Funktionsumfang. Die Aufwärtskompatibilität zu C wurde jedoch gewahrt. C++3
ist bis auf einige Ausnahmen eine Obermenge der Programmiersprache C. Selbst CBibliotheken sind weiterhin (noch) uneingeschränkt benutzbar.
Viele Anzeichen sprechen dafür, daß C++ zur Programmiersprache der neunziger
Jahre wird. Die Gründe für diese Entwicklung sind offensichtlich:
-
-
-
C++ besitzt die wesentlichen Merkmale einer objektorientierten Programmiersprache, zwingt der Anwendung dieses Paradigma jedoch nicht auf, sondern läßt
sich auch als verbessertes C einsetzen.
Übersetzer zu C++ sind praktisch unter allen bekannten Betriebssystemen
verfügbar und erzeugen relativ effektiven Code. Die derzeit laufende
Standardisierung durch das ANSI Komitee X3J16 verspricht außerdem für die
Zukunft eine portable Sprachdefinition.
C++-Programme sind mit den großen Mengen existierender C-Software kombinierbar. Darüberhinaus ist bereits das Angebot an kommerziell verfügbaren CKlassenbibliotheken unüberschaubar.
C++ hat allerdings auch einen Nachteil: C++ ist nicht einfach.
Grundlage für C++ (in diesem Skriptum) bildet die Sprachdefinition der Version 2.1
von Margaret Ellis und Bjarne Stroustrup4 und der im Sommer 1998 verabschiedete
internationale C++-Standard (ISO/IEC 14882)
1
Beschrieben erstmals in: Stroustrup, Bjarne: "Classes: An Abstract Data Type Facility for the C Language",
ACM SIGPLAN Notices, January 1982
2 Stroustrup, Bjarne: "Data Abstractions in C", AT&T Bell Labaratories Technical Journal, October 1984
3 dokumentiert in: Stroustrup, Bjarne: "The C++ Language", Addison-Wesley, 1986 bzw. Stroustrup, Bjarne:
"Die C++ Programmiersprache", Addison-Wesley, 2. überarbeitete Auflage, 1992
Ellis, Margaret und Stroustrup, Bjarne: "The Annotated C++ Reference Manual", Addison-Wesley, Reading,
MA, 1990
4 Ellis, Margaret und Stroustrup, Bjarne: „The Annotated C++ Reference Manual, Addison-Wesley, Reading,
MA, 1990
8
Die Programmiersprache C++
1.2 Ein einführendes Beispiel
1.2.1 Aufgabenstellung und Lösungsvorschlag zu einem Sortierverfahren
Aufgabenstellung: Schreibe ein Programm, das die in einem Arbeitsspeicherfeld
(„array“) gespeicherten ganzen Zahlen nach einem einfachen Sortierverfahren
(„Sortieren durch Austauschen, Bubble-Sort“) in aufsteigende Sortierreihenfolge
bringt.
Lösungsverfahren (Algorithmus): Im Bubble-Sort werden Zahlen eines Arbeitsspeicherfelds nach folgendem Schema miteinanander verglichen:
Zuerst wird der letzte mit dem vorletzten Schlüssel (Zahl) verglichen. Ist der letzte bspw. kleiner als
der vorletzte Schlüssel, dann werden im Falle der aufsteigenden Sortierung die Zahlen miteinander
vertauscht. Der gleiche Vorgang wiederholt sich dann mit dem vorletzten und drittletzten Schlüssel,
dann mit dem drittletzten und viertletzten Schlüssel. Dies wird bis zum Vergleich des zweiten mit dem
ersten Schlüssel fortgesetzt. Nach (N - 1) Vergleichen befindet sich der kleinste Schlüssel auf der
ersten Position. Nach dem gleichen Schema kann danach mit (N - 2) Vergleichen der zweitkleinste
Schlüssel auf die zweite Position gebracht werden.
Dies wird fortgesetzt bis schließlich nur ein einziger Schlüssel vorhanden ist, der
dann auf der richtigen Position stehen muß.
Bsp.:
1
37 37 37
22 22 22
18 18 9
9
9 18
25 25 25
37
9
22
18
25
9
37
22
18
25
2
3
9 9 9
37 37 18
22 18 37
18 22 22
25 25 25
9
18
37
22
25
4
9
18
22
37
25
9
18
22
25
37
Abb. 1.1-1: Sortiertabelle zum Sortieren durch Austauschen
Alternativ zu dem vorliegenden Lösungsverfahren kann man auch jeweils den ersten Schlüssel gegen
alle weiteren Schlüssel im Arbeitsspeicherfeld vergleichen und so an der ersten Position den kleinsten
Schlüssel nach (N-1) Vergleichen ermitteln. Danach vergleicht man jeweils den Schlüssel aus der
zweiten Position mit allen nachfolgenden Schlüsslwerten. Nach (N-2) Vergleichen steht der
zweitkleinste Sachlüssel an der zweiten Position. Bei Fortsetzung dieser Verfahrensweise ist
schließlich nur noch ein einziger Schlüssel vorhanden, der dann auf der richtigen, der letzten Position
steht.
Bsp.:
1
37 22 18 9 9
22 37 37 37 37
18 18 22 22 22
9
9 9 18 18
25 25 25 25 25
2
9
22
37
18
25
3
9
18
37
22
25
9
18
37
22
25
9
18
22
37
25
4
9
18
22
37
25
9
9
18
22
25
37
Die Programmiersprache C++
Vorschlag zur Implementierung5:
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Das folgende Programm enthaelt 3 Funktionsaufrufe:
1. eingabe()
Zahlen werden in ein Feld eingelesen
2. bsort()
Die Zahlen werden sortiert
3. ausgabe()
Feldelemente werden ausgegeben
*/
*/ #include <iostream.h>
*/ #include "a:\eing.h"
// eingabe()
*/ #include "a:\ausg.h"
// ausgabe()
*/ #include "a:\sort.h"
// bsort()
*/
*/ int n;
// Vereinbarung von Variablen
*/ int x[20];
*/
Hauptprogramm */
*/ main()
*/ {
*/ eingabe(x,n);
*/ cout << "Ausgabe unsortiert: " << endl;
*/ ausgabe(x,n);
*/ cout << "Sortieren durch Austauschen" << endl;
*/ bsort(x,n);
*/ ausgabe(x,n);
*/ return 0;
*/ }
1.2.2 Der Aufbau eines C++-Programms
1.2.2.1 Das Layout des Programms
Der äußere Aufbau eines Programms wird von C++ nicht vorgeschrieben. Die
Markierung, die ein Programm in einzelne Anweisungen zerlegt, ist das Semikolon
(„;“). Jede Anweisung im Quellcode muß mit einem Semikolon enden. Geschweifte
Klammern kennzeichnen den Anfang und das Ende eines Blocks.
C++ ist eine formatfreie Sprache. Das Aussehen, die Gestalt des Programms kann
man in weiten Grenzen selbst bestimmen. C++ ist beim äußeren Aufbau des
Programms sehr großzügig. Bei der Schreibweise von Funktionen oder
Variablennamen ist es aber sehr genau. C++ ist case sensitive, d.h.: Es
unterscheidet zwischen Groß- und Kleinschreibung.
Ein Quellprogrammtext ist in C++ in einer Datei6 zusammengefaßt. Dateien sind die
grundlegenden Programmeinheiten in C++ und durch die Extension .C bzw. .CPP
bzw. .CC gekennzeichnet. Jedes C++-Programm ist eine Sammlung von Funktionen.
Die Ausführung eines Programms startet immer mit einer Funktion, die den Namen
main hat.
5
6
PR12101.CPP
PR12101.CPP
10
Die Programmiersprache C++
Includes
Einlesen von Quellcode durch den Präprozessor
Defines
Anweisungen an den Präprozessor, z.B. zur Definition
von Konstanten und Makros bzw. zur bedingten
Übersetzung
Globale Variablen
Definition von Variablen, die allgemein gelten sollen
funktion_1()
Funktionen, die direkt oder indirekt von main()
aufgerufen werden
...............
funktion_n()
main()
Funktion, in der die Ausführung des Programms startet
Häufig wird auch folgende Struktur für ein C- bzw. C++-Programms benutzt:
Includes
Einlesen von Quellcode durch den Präprozessor
Defines
Anweisungen an den Präprozessor, z.B. zur Definition
von Konstanten und Makros bzw. zur bedingten
Übersetzung
Globale Variablen
Definition von Variablen, die allgemein gelten sollen
Funktionsprototypen
Unterprogrammschnittstellen, die direkt oder indirekt
von main() aufgerufen werden, aber erst im Anschluss
definiert sind.
main()
Funktion, in der die Ausführung des Programms startet
Funktions-Definitionen
der Funktionsprototypen
Funktionen, die direkt oder indirekt von main() aufgerufen
werden
Abb. 1.1-2: Struktur eines C++-Programms
11
Die Programmiersprache C++
1.2.2.2 Übersetzung
Vor der Übersetzung durchsucht der Präprozessor das zu übersetzende Programm
nach besonderen Direktiven. So fügt bspw.
#include <iostream.h> bzw7. #include <iostream>
den Inhalt der (Header-) Datei iostream.h in das Programm ein. In der
nachfolgenden Analysephase der Compilierung wird die Quellcode-Datei in Symbole
und Whitespace-Zeichen zerlegt.
Symbole sind wortähnliche Einheiten. Sie ergeben sich als Folge von Operationen,
die der Compiler und sein Präprozessor mit dem zu übersetzenden Programm
durchführen. C++ kennt 6 Arten von Symbolen: „Schlüsselwort, Bezeichner,
Konstante,
String-Literal,
Operator,
Interpunktionszeichen
(Trennzeichen,
Seperatoren)“.
Schlüsselworte sind für spezielle Zwecke reservierte Worte, die nicht als Bezeichner
verwendet werden dürfen.
asm
class
double
friend
long
register
struct
unsigned
auto
const
else
goto
new
return
switch
virtual
break
continue
enum
if
operator
short
template
void
case
default
extern
inline
private
signed
this
volatile
catch
delete
float
int
protected
sizeof
typedef
while
char
do
for
interrupt
public
static
union
Abb. 1.1-3: Schlüsselworte in C++
Bezeichner sind beliebige Namen von beliebiger Länge für Klassen, Objekte,
Funktionen, benutzerdefinierte Datentypen, usw. Bezeichner können die Buchstaben
a bis z, A bis Z, den Unterstrich _ und die Ziffern 0 bis 9 enthalten. Das erste
Zeichen muß ein Buchstabe oder ein Unterstrich sein. Bezeichner können beliebige
Namen sein, die diesen Regeln entsprechen.
Konstante sind Symbole, die für numerische Werte oder Zeichenwerte
(Zeichenkonstanten,
Stringkonstanten)
stehen.
Stringkonstante
(Zeichenkettenkonstante) bestehen aus einer Reihe beliebig vieler Zeichen, die in
Anführungszeichen eingeschlossen sind, z.B.: "Sortieren durch Austauschen"
Interpunktionszeichen bestehen aus einem der folgenden Symbole: [ ] ( ) { }
, ; : ... * = #
[ ] beinhalten einfache und mehrdimensionale Array-Indizes, z.B. int x[20];
( ) fassen Ausdrücke zusammen, isolieren konditionale Ausdrücke, präsentieren
Funktionsaufrufe und Funktionsparameter, z.B.:
main()
eingabe(x,n);
ausgabe(x,n);
bsort(x,n);
{ } markieren den Beginn und das Ende eines Anweisungsblocks.
7
Hinweis: Falls ein Compiler #include <iostream> nicht versteht, entspricht er noch nicht dem C++Standard
12
Die Programmiersprache C++
Das Komma „,“ trennt Elemente in einer Funktions-Argumentenliste.
Das Semikolon „;“ dient als Endekriterium einer Anweisung.
Mit dem Doppelpunkt „:“ wird ein Label gekennzeichnet.
Whitespace-Zeichen8 können sein: Leerzeichen, horizontale und vertikale
Tabulatoren, Zeichenvorschübe, Kommentare. Sie sind für das Erkennen von
Anfang und Ende eines Symbols geeignet. Die beiden folgenden Sequenzen
int n; int x[20];
und
int n;
int x[20];
sind (lexikalisch) identisch und werden in diesselben Symbole zerlegt:
int
n
;
int
x
[
20
]
;
Falls Whitespace-Zeichen innerhalb alphanumerischer Zeichenketten stehen, sind
sie vom normalen Bearbeitungsvorgang (Parsing) ausgenommen, z.B.:
char name[] = "Fachhochschule Regensburg";
Ein Sonderfall ist: Das Auftreten eines Backslash (\) vor dem Zeilenvorschubzeichen. Der Backslash und der Zeilenvorschub werden ignoriert mit der
Konsequenz, daß 2 physisch vorhandene Zeichen als Einheit betrachtet werden.
Aus
"Fachhochschule \
Regensburg"
wird "Fachhochschule Regensburg";
8
Diese Zeichen sind über die Funktion isspace in ctype.h definiert
13
Die Programmiersprache C++
1.2.2.3 Entwicklungszyklus eines C++-Programms
Am Anfang steht die Eingabe des Quellcodes (Programmtext) mit einem Editor.
Integrierte Entwicklungsumgebungen (IDEs) haben einen speziell auf
Programmierzwecke zugeschnittenen Editor, der auf Tastendruck oder Mausklick die
Übersetzung anstößt. Alternativ besteht die Möglichkeit, Compiler und Linker im
Shell- oder MS-DOS-Fenster in der Kommandozeile zu starten, z.B. für das
Programm PR12101.CPP und dem GNU-Compiler:
g++ -c pr12101.cpp
g++ -o pr12101.exe pr12101.o
// Übersetzen: pr12101.o wird erzeugt
// Linken
bzw. zusammengefaßt:
g++ -o pr12101.exe pr12101.cpp
Es wird vorausgesetzt, daß der Compiler weiß, wo die Header-Files zu finden sind. Mit dem GNUCompiler wird eine Batch-Datei mit dem Namen cygnus.bat ausgeliefert, die alle nötigen
Einstellungen vornimmt.
Es folgt die Übersetzung durch den Compiler und dann ein abschließender LinkerLauf. Im Fehlerfall ist wieder der Editor an der Reihe.
C++-Programme können auch aus getrennt übersetzten Moduln bestehen, die durch
den Linker zusammengebunden werden. Außerdem benutzt nahezu jedes
Programm Routinen aus mitgelieferten Bibliotheken. Der „Linker“ bindet den
Objectcode der übersetzten Einheiten mit dem Objectcode der Bibliotheken
zusammen und erzeugt ein ausführbares Programm, das nun gestartet werden
kann. Der Aufruf des Programms bewirkt, daß der Lader (- eine Funktion des
Betriebssystems -) das Programm in den Arbeitsspeicher lädt und startet.
Die aktuellen Versionen (gcc version 2.95.3-5, cygwin spezial) erstellen ein
.exe-File nach dem Aufruf
gcc pr12101.cpp –lstdc++
14
Die Programmiersprache C++
Editor
Source
Header
Präprozessor
Compiler
Librarien
(Verwaltung und Pflege der Objektmoduln)
Objekt
Linker
Übernahme eigener Objekte in die Bibliothek
Bibliotheken
Programm
Abb. 1.1-4: Ablaufdiagramm C++-Programmentwicklung
1.2.2.4 Kommentare und Präprozessor-Direktiven
Kommentare
In C++ leiten doppelte Schrägstriche (//) einen Kommentar ein, d.h.: Alles, was in
der Zeile hinter diesen Zeichen folgt, wird vom Compiler ignoriert. Sollen
Kommentare über mehrere Zeilen gehen, benutzt man die aus C bekannte
Kombination „/* .... */“.
Das vorliegende Programm beginnt mit einem Kommentar (eingeschachtelt durch /*
... */). Kommentare können auch mit dem Zeichen // eingeleitet werden (vgl.
Zeile 10 .. 13). Nach // ist der Rest der Zeile Kommentar.
Der Präprozessor
Er dient zur Bearbeitung des Quelltextes, d.h.: Er sucht und ersetzt bestimmte
Begriffe im Programm-Quelltext oder sorgt dafür, daß bestimmte Programmteile
15
Die Programmiersprache C++
compiliert werden und andere nicht. Was der Präprozessor macht, kann man mit
einem bestimmten Befehl steuern. Diese Befehle beginnen mit einem Nummernzeichen (#), dem unmittelbar anschließend das Schlüsselwort9 folgt:
#define
#endif
#ifdef
#line
#elif
#error
#ifndef
#pragma
#else
#if
#include
#undef
#include
In Zeile /* 9 */ steht die Präprozessor-Direktive #include, die die Datei "iostream.h"
einbezieht. In der Regel handelt es sich dabei um sog. Header-Dateien. Sie
enthalten Definitionen, die für bestimmte Funktionen oder Objekte benötigt werden.
Es gibt eine Vielzahl dieser Header-Dateien10 zur Abdeckung spezieller Bereiche. So
muß z.B. die Datei string.h mit einem include-Befehl eingebunden werden, falls
Zeichenketten-Funktionen (strcpy, strcat) benötigt werden. Welche HeaderDatei benötigt wird, ist im jeweiligen Referenzhandbuch der Funktionen beschrieben.
Hinter dem Schlüsselwort include folgt der Name der Datei, die in das Programm
einzukopieren ist. Der Dateiname ist häufig in spitzen Klammern (< > )
eingeschlossen, d.h.: Diese Datei wird in den voreingestellten „include-Pfaden“
gesucht, z.B.: #include <iostream.h>. Ist der Dateiname in Anführungszeichen
eingeschlossen, dann wird diese Datei zuerst im aktuellen Verzeichnis gesucht und
dann erst im voreingestellten Pfad, z.B.:
#include "a:\ausg.h"
#include "a:\sort.h"
#define
Mit #define kann man einer Konstanten einen Namen geben, z.B.: #define
MAXWERT 100
Üblicherweise schreibt man zur besseren Unterscheidung von Variablen die mit
#define definierte Konstante in Großbuchstaben.
C++ kennt aber bessere Methoden (z.B. const) zur Defintion von Konstanten.
Umfassend interpretiert, definiert die Direktive #define ein sog. Makro. Im
einfachen Fall (ohne Parameter) schreibt man #define Makro_Bezeichner <SymbolSequenz>
Jedes Auftreten von Makro-Bezeichner im Quelltext nach dieser Steuerzeile wird
durch die (möglicherweise leere) Symbolsequenz ersetzt.
Eine Makrodefinition ist mit dem Zeilenende abgeschlossen (Verlängerung über "\"
als unmittelbar letztes Zeichen), z.B.:
#define FH
#define PI 3.141592653897953
#define quadrat(a) (a * a)
// leerer Text
// expansionsfähig
// parametrisiert
In der dritten Form ist der dem Makro-Bezeichner zugeordnete Text parametrisiert
und kann den individuellen Gegebenheiten angepaßte werden, z.B 11.:
9
Je nach Compiler können noch einige weitere Begriffe hinzukommen
Zu jedem Compiler gehören zahlreiche Header-Dateien, die gewöhnlich in einem Verzeichnis mit dem Namen
INCLUDE abgelegt sind
11 vgl. PR12203.CPP
10
16
Die Programmiersprache C++
#include <iostream.h>
#define PI 3.14159265389793
#define quadrat(a) (a * a)
int main()
{
int zahl;
cout << "Zahl eingeben:\n";
cin >> zahl;
cout << "\nDie Kreisflaeche betraegt: "
<< quadrat(zahl) * PI;
}
Unerwünschte Seiteneffekte können allerdings auftreten. So würde z.B.
quadrat(zahl - zahl) * PI keineswegs das erwartete Ergebnis 0 anzeigen.
Ein korrektes Ergebnis würde in diesem Fall allerdings die Makrodefinition #define
quadrat(a) ((a) * (a)) erzwingen.
#undef
Mit dieser Direktive kann ein Makro außer Kraft gesetzt werden: #undef MakroBezeichner. Diese Zeile entfernt jede vorherige Symbol-Sequenz aus dem MakroBezeichner. Die Makro-Definition wird sofort ungültig, der Makro-Bezeichner ist von
da ab undefiniert.
#ifdef und #ifndef
Mit diesen Bedingungsdirektiven kann geprüft werden, ob ein Bezeichner derzeit
definiert ist oder nicht. Die Zeile #ifdef Bezeichner besitzt die gleiche Wirkung
wie #if 1, falls Bezeichner derzeit definiert ist, und die Wirkung #if 0 ,falls
Bezeichner nicht definiert ist.
Mit der #ifdef-#endif - Anwisung kann überprüft werden, ob eine Konstante
bereits ein anderer Stelle definiert wurde. Die Anweisung #ifndef-#endif leistet
das Gegenteil, z.B.:
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif
Die ersten beiden
höchstwahrscheinlich:
Präprozessoranweisungen
von
iostream.h
lauten
#ifndef __IOSTREAM_H
#define __IOSTREAM_H
Der Zweck dieser Angaben ist: Verhinderung von Mehrfachdefinitionen derselben
Datei. Sobald die Datei name.h zum 1. Mal vom Präprozessor bearbeitet wird, ist
der Name __NAME_H definiert. Bei einer weiteren Inklusion überspringt dann der
Präprozessor den gesamten Text bis zum passenden #endif.
#if, #elif, #else, #endif
Sie funktionieren wie normale Bedingungsanweisungen, z.B. überprüft die #ifDirektive, ob ein angegebener Ausdruck einen wahren (d.h. von Null verschiedenen)
Wert zur Übersetzungszeit ergibt und aktiviert bzw. deaktiviert Teile des Programms.
17
Die Programmiersprache C++
Bei allen Präprozessoranweisungen muß am Ende ein Zeilenvorschub erfolgen. Ein
#if und #else dürfen bspw. nicht in derselben Zeile stehen.
#pragma
Diese Direktive erlaubt die Definition implementierungsspezifischer PräprozessorDirektiven der Form #pragma Direktive-Name. Über #pragma kann bspw.
Borland C++ beliebig eigene Dateien definieren12, ohne dabei mit anderen Compilern
in Konflikt zu geraten, die #pragma ebenfalls unterstützen. Falls der Compiler
„Direktive-Name“ nicht kennt, dann ignoriert er die #pragma-Direktive ohne
Fehlermeldung oder Warnung.
1.2.2.5 Standardein-/ Standardausgabe
Ein-/ Ausgabefunktionen im einführenden Bsp.
In der Datei „iostream.h“ befinden sich u. a. die Deklarationen der Funktionen zur
Ein-, Ausgabebehandlung. Die Standardeingabe (normalerweise die Tastatur) ist
hier durch den Eingabestrom cin festgeglegt. Die Ausgabe zum Terminal ist
vordefiniert über cout. Der Operator << ist für den Linksshift zur Bitmanipulation
vorgesehen. Er wird in iostream.h für Ausgabezwecke überladen und bestimmt die
Richtung zur Ausgabe, z. B.:
#include
main()
{
int
x
float y
cout <<
}
<iostream.h>
= 124;
= 1.5;
"x = " << x << " y = " << y << "\n";
„<<“ ist hier sogar mehrfach überladen, für jeden Datentyp des rechten Operanden
ist hier eine eigene Version vorgesehen.
Mit „cin >> “ werden Eingaben über die Tastatur der angegebenen Variablen
zugewiesen:
void eingabe(int *x, int& n)
{
cout << "Anzahl Elemente? \n";
cin
>> n;
cout << "Elementweise Eingabe: \n";
for (int i = 0; i < n; i++)
cin >> x[i];
}
Diese Prozedur ist in der Datei „eing.h“ enthalten und wird über das Kommando
#include "a:\eing.h" in das vorliegende Programm einbezogen (vgl. Zeile
/* 10 */). Die Hochkommata geben an, daß die Datei nicht unter dem üblichen
Verzeichnis, sondern unter dem angegebenen Pfadnamen zu finden ist. Auf das
übliche Verzeichnis wird durch spitze Klammern, z.B. #include <iostream.h>
hingewiesen.
12
vgl. Borland C++ 4.0, Programmierhandbuch S. 206
18
Die Programmiersprache C++
Der Prozedurkopf von „eingabe“ trägt den Spezifizierer void. Damit ist „eingabe“
als Funktion definiert, die keinen Rückgabewert liefert. Wird kein Ergebnistyp
angegeben, so wird als Ergebnistyp int angenommen.
Die Parameterliste von „eingabe“ zeigt zwei Einträge. Der erste Eintrag beschreibt
einen Referenzparameter auf das Feld x (in Zeile /* 15 */ global definiert). In C++
kann ein formaler Parameter einer Funktion als Referenz durch Nachstellen des
Zeichen & nach dem Typ definiert werden. Dadurch wird dem Compiler signalisiert,
daß der Wert als Referenz (call by reference) zu übergeben ist. In konventionellem
C erfolgt die Paramterübergabe grundsätzlich mit „call by value“. Zur Veränderung
von Daten muß man Zeiger als Parameter übergeben. Der hier verwendete erste
Referenzparameter bezieht sich auf das eindimensionale Feld (C-Array):
int x[20];
Falls 5 Komponenten dieses C-Array mit ganzen Zahlen belegt sind, könnte der
Speicherplatz dafür so aussehen:
.......
x[0]
1
Startadresse x
x[1]
2
Startadresse + 413
x[2]
3
Startadresse + 8
x[3]
4
Startadresse + 12
x[4]
5
Startadresse + 16
.......
Abb. 1.1-5: „int-Array“-Tabelle
Der Name des C-Arrays „x“ zeigt auf die Startadresse. Der Zugriff auf ein Element ist
durch den Indexoperator [] möglich. Die Numerierung der Indexpositionen beginnt
bei 0. Zwischen den eckigen Klammern des Indexoperators wird für den Zugriff auf
die Komponente die (relative) Tabellenposition eingetragen. Für C-Arrays gelten
folgende Regeln:
-
Array-Indizes beginnen mit 0. Das erste Element im C-Array hat den Index 0, das zweite Element
den Index 1, usw.
Array-Größen müssen Kompilierzeitkonstanten sein, d.h. der Compiler muß zur Kompilierzeit
wissen, wieviel Platz er für das Array allokieren kann.
Ein C- Array kann kein sogenannter L-Wert14 sein
Zur Ausgabe der Feldelemente wurde in
das vorliegende Programm die
Funktionsprozedur „ausgabe“ (vgl. Zeile /* 22 */) aufgenommen:
const zeilenLaenge = 12;
// Anzahl der Elemente je Zeile
void ausgabe(int* x, int n)
{
cout << "(" << n << ") <";
13
14
Jeder Integer-Wert belegt einen Speicherbereich von 4 Bytes
L-Wert bezeichnet eine Größe, die auf der linken Seite einer Zuweisung stehen darf.
19
Die Programmiersprache C++
for (int i = 0; i < n; ++i)
{
if (i % zeilenLaenge == 0 && i)
cout << "\n\t";
cout << x[i];
// Trennen durch , Ausnahme letztes Element
if (i % zeilenLaenge != zeilenLaenge - 1 && i != n - 1)
cout << ", ";
}
cout << ">\n";
}
Die Funktionsprozedur „ausgabe“ bezieht sich auf eine global definierte Konstante.
Mit dem Schlüsselwort const lassen sich Objekte definieren, die einen Bezeichner
zu einer Konstanten erklären, dessen Wert nicht verändert werden darf.
Stream-Ausgabe
Die Stream-Ausgabe wird mit <<15 erzeugt. Der standardmäßige LinksschiebeOperator16 ist für Ausgabe-Operationen überladen. Sein linker Operand ist ein Objekt
der Klasse ostream. Sein rechter Operand ist ein beliebiger Typ 17, für den die
Stream-Ausgabe definiert wurde, z.B.: cout << "hallo!\n";
Der Operator << wirkt von links nach rechts und liefert eine Referenz auf das
ostream-Objekt, für das er gerufen wurde. Dadurch wird es möglich, mehrere
Ausgaben aneinanderzureihen, z.B.:
#include <iostream.h>
int main()
{
int x = 124;
float y = 1.5;
cout << "x = " << x << "y = " << y << "\n";
}
Zeichenketten enthalten besondere Zeichen, die nicht ausgedruckt werden, z.B.:
cout << "Elementweise Eingabe: \n";
cout << "\n\t"
Die Zeichenfolge „\n“ nennt man Escape-Sequenz. Escape-Sequenzen werden
durch einen „backslash“ eingeleitet, dem ein oder mehrere Zeichen folgen. Das „n“
bedeutet „new-Line“ (und bewirkt einen Zeilenvorschub). Andere Zeichen, z.B. ‘\t‘
bewirken einen Tabulatorsprung. Escape-Sequenzen können an beliebiger Stelle
und beliebig oft in der Zeichenkette auftauchen. Sie können in Hochkomma ('\n') und
in Anführungszeichen ("\n")stehen.
Folgende grundlegende Datentypen werden direkt unterstützt: char, short,
int, long, char*18, float, double, long double und void*.
Der Zeiger-Übergabeoperator void* wird zur Anzeige von Zeigeradressen
verwendet, z.B.:
int i;
cout << &i;
// zeigt die Zeigeradresse in hexadezimaler Schreibweise an
15
vgl. 3.4.2
Linksshift zur Bitmanipulation
17 Das sind die grundlegenden Typen oder beliebige, überladene Typen
18 Behandlung als Zeichenkette
16
20
Die Programmiersprache C++
Die Formatierung der Ein-/ Ausgabe wird durch verschiedene Formatierungs-Flags19
bestimmt, die in der Klasse ios definiert sind. Diese Flags werden über die
Elementfunktionen flags, setf und unsetf erkannt und ersetzt.
Formatvariable können über einen speziellen, funktionsähnlichen Operator verändert
werden, der auch als Manipulator bezeichnet wird. Manipulatoren20 nehmen eine
Stream-Referenz als Argument entgegen und liefern eine Referenz auf denselben
Stream zurück.
Stream-Eingabe
Sie verwendet den überladenen Rechtsschiebe-Operator21 >>. Der linke Operand
von >> ist ein Objekt der Klasse istream. Der rechte Operand kann ein beliebiger
Typ sein, der für die Stream-Eingabe definiert wurde. Der Operator >> sorgt bei der
Eingabe dafür, daß automatisch die nötigen Umformatierungen vorgenommen
werden, z.B.:
int zahl;
cin >> zahl;
bewirkt: Ein Folge von Ziffernzeichen wird bis zu einem Nicht-Ziffernzeichen
eingelesen und in die interne Darstellung einer int-Zahl umgewandelt.
Die Auswertung durch den >>-Operator hat bestimmte Eigenschaften:
-
Führende Zwischenraumzeichen werden ignoriert22.
Zwischenraumzeichen werden als Endekennung genutzt.
Andere Zeichen werden entsprechend dem verlangten Datentyp interpretiert.
Umlenken Ein-, Ausgabe
cin und cout können auf Betriebssystemebene mit < bzw. > umgelenkt werden. So
können mit dem Umlenkungszeichen < und > die zugehörigen Eigabedaten bspw.
aus einer Datei anstelle von der Tastatur geholt werden, und anstelle der
Bildschirmausgabe kann in eine Datei geschrieben werden.
19
vgl. 3.4.2
vgl. 3.4.2
21 vgl. 3.4.3
22 Sollen Zwichenraumzeichen nicht ignoriert werden, ist die Funktion get() zu verwenden, die zum Einlesen
einzelner Zeichen (keine Zahlen) verwendet werden kann, z.B.: // einzelnes Zeichen einlesen
char zch;
cin.get(zch);
20
21
Die Programmiersprache C++
1.2.2.6 Hauptprogramm
In den Zeilen /* 18 */ bis /* 27 */ steht das durch das Schlüsselwort main
gekennzeichnete Hauptprogramm. Jedes ausführbare C++-Programm muß genau
eine Funktion "main" enthalten, mit deren Aktivierung das Betriebssystem die
Kontrolle an das Programm übergibt. Das Programm endet üblicherweise mit der
letzten Anweisung in main(), mit der ein Fehlercode an das BS übergeben werden
sollte. Mit "return" wird die Ausführung eines Funktionsaufrufs beendet. Die
geschweiften Klammern in Zeile /* 19 */ bzw. /* 27 */ bestimmen den Beginn ({) und
das Ende (}) eines Blocks.
Das sog. Hauptprogramm des einführenden Beispiels könnte auch so aussehen:
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
18
19
20
21
22
23
24
25
26
27
*/ int main() // kann auch so geschrieben werden: int main(void)
*/ {
*/ eingabe(x,n);
*/ cout << "Ausgabe unsortiert: " << endl;
*/ ausgabe(x,n);
*/ cout << "Sortieren durch Austauschen" << endl;
*/ bsort(x,n);
*/ ausgabe(x,n);
*/ return 0; // kann entfallen
*/ }
Der Aufruf der Funktion void exit(int) (deklariert in <stdlib.h>) terminiert das
Programm. Eine return-Anweisung bewirkt das gleiche, wie der Aufruf von exit()
mit dem return-Wert als Argument. Der Rückgabetyp von main() soll int sein23.
Return kann auch weggelassen werden, dann wird automatisch 0 zurückgegeben.
Die beiden folgenden Formen von main() sind mindestens gefordert und werden
daher von jedem Compilerhersteller zur Verfügung gestellt:
int main()
{
...
return 0;
}
// Exit-Code
bzw.
int main(int argc, char* argv[])
{
...
return 0; // Exit-Code
}
Die zweite Variante verwendet Zeiger (char*) und C-Arrays. Es bleibt dem
Hersteller eines Compilers überlassen, ob er weitere Versionen anbietet.
23
ist implementierungsabhängig
22
Die Programmiersprache C++
1.2.2.7 Kommandozeilenversionen bzw. Konsolenanwendungen von Visual C++ Version
6.0 bzw. Dev-C++
Das vorliegende Programm des einführenden Beispiels benutzt die
Kommandozeilenversion des GNU-Compilers und ist eine Konsolenanwendung, d.h.
eine Anwendung ohne eine grafische Benutzeroberfläche. Eine Konsolenanwendung wird unter Windows im (MS-DOS-) Eingabefenster ausgeführt. Das weit
verbreitete System Windows unterstützt im starken Maße das Arbeiten mit
grafischen Benutzeroberflächen (GUI). So sind die C++-Compiler für Windows (z.B.
C++Builder, Visual C++) mit einer integrierten Entwicklungsumgebung (IDE)
ausgestattet, von der alle für die Programmerstellung wichtigen Werkzeuge und
Informationen aufgerufen werden können. Die wichtigsten C++-Compiler unter
Windows ermöglichen auch den Einsatz von Kommandozeilenversionen.
1. Kommandozeilenversion von Visual C++ Version 6
Dieser Compiler befindet sich im BIN-Verzeichnis von Visual C++. Seine EXE-Datei
heißt: cl.exe. Dieser Compiler wird aufgerufen über die (MS-DOS-)
Eingabeaufforderung24, die unter Windows über das Menü START/PROGRAMME
aufgerufen wird. Zum Aufruf des Compiler muß bekannt sein, in welchem
Verzeichnis die EXE-Datei des Compiler zu finden ist. Weiterhin muß der Compiler
wissen, wo die „include“-Dateien und die „lib“-Dateien zu finden sind. Zu diesem
Zweck muß die Systemumgebung durch Erweiterung des Pfads (PATH) und Setzen
einiger Umgebungsvarianten angepaßt werden. Microsoft liefert mit dem Visual C++
- Compiler eine Batch-Datei mit dem Namen vcvars32.bat aus, die alle nötigen
Einstellungen vornimmt.
Folgende Vorgehensweise empfieht sich:
1. Aufruf des (MS-DOS)-Eingabefensters über START/PROGRAMME
2. Wechseln im Eingabefenster mit dem DOS-Befehl cd in das BIN-Verzeichnis von C++. In diesem
Verzeichnis sollte sich auch vcvars.bat befinden.
3. Aufruf von vcvars32.bat
Bsp.: Erstellen einer Konsolenanwendung mit cl.exe
1. Aufruf des Notepad-Editor oder irgendeines beliebigen anderen Editors, mit dem man ASCIITextdateien erstellen kann. Eingabe des Quelltexts, z.B.:
#include <iostream.h>
int main()
{
cout << "Herzlich Willkommen" << endl;
return 0;
}
2. Speichern des Quelltexts
3. Aufruf der Eingabeaufforderung; Ausführen von vcvars32.bat; Wechseln in das Verzeichnis, in
dem die Quelldatei steht
4. Aufruf des Compiler zum Erstellen des Programms
5. Falls das Übersetzen fehlerfrei erfolgt ist, Ausführen vom fertigen Programm
24
Unter fensterbasierenden Betriebssystem sind Konsolenanwendungen darauf angewiesen, daß ihnen das
Betriebssystem ein Standardfenster zuweist, über das Ein- und Ausgabe erfolgen können. Dieses Fenster nennt
man üblicherweise Konsole oder Konsolenfenster; unter Windows heißt es Eingabeaufforderung.
23
Die Programmiersprache C++
Abb.: Aufruf des Compiler von der Kommandozeile
Standarausgabe
für
Konsolenanwendungen
ist
das
Fenster
der
Eingabeaufforderung, Standardeingabe ist die Tastatur. Nur wenn die
Eingabeaufforderung den Fokus hat, werden Tastatureingaben an die im Fenster der
Eingabeaufforderung ausgeführte Konsolenanwendung geschickt.
2. Konsolenanwendung in IDE unter Visual C++ Version 6
Visual C++ umfaßt zwei Compiler
-
eine Befehlszeilen-Compiler
eine alles-in-einem integrierte Entwicklungsumgebung (IDE)
Die IDE erlaubt nicht
-
einfach das Anlegen einer Datei
die Eingabe des Quelltexts
das Erstellen einer ausführbaren Datei aus dem Quelltext
Statt dessen wird verlangt, daß alle Dateien, die zu einem Programm gehören, in
einem Projekt25 zusammen gefaßt werden.
Bsp.: Erstellen einer Konsolenanwendung in IDE
1. Aufruf von Visual C++
2. Anlegen eines Arbeitsbereichs und eines Projekts
Aufruf des Befehls DATEI/NEU; Anzeigen der Seite PROJEKTE; Eingabe eines Namens für das
Projekt und Auswahl eines übergeordneten Verzeichnisses für das Projekt. Visual C++ legt unter
diesem Verzeichnis ein Unterverzeichnis für das Projekt an, das den gleichen Namen trägt wie das
Projekt.
25
Projekte werden ihrerseits wieder in Arbeitsbereiche organisiert. Das ist vor allen interessant, wenn mehrere
ausführbare Dateien irgendwie zusammengehören (bspw. eine EXE-Datei und eine DLL, die von der EXE-Datei
aufgerufen wird).
24
Die Programmiersprache C++
Abb.: Anlegen eines neuen Projekts
3. Drücken auf OK.
Es erscheint ein Dialogfeld, in dem ausgewählt werden kann, wie weit das Projekt von Visual C++
schon vorab konfiguriert und mit Code ausgestattet werden soll.
Wähle die Option26 EIN LEERES PROJEKT; Drücke auf FERTIGSTELLEN.
4. Aufnahme einer Quelltextdatei in das Projekt.
Auf der Seite DATEIEN Eingabe eines Namens für die hinzuzufügende Datei;
Klick auf OK zur Aufnahme der Datei in das Projekt.
26
Verzicht auf jegliche weitere Unterstützung durch die IDE
25
Die Programmiersprache C++
Abb.: Hinzufügen einer Quelltextdatei zu einem Projekt
5. Eingabe Quelltext
Die Datei wird in den integrierten Quelltexteditor geladen, der Quellcode kann direkt eingegeben
werden.
6. Erstellen des Programms
Im Ausgabefenster, das in dem unteren Rand des IDE-Rahmenfensters integriert ist, wird der
Fortgang des Erstellungsprozesses angezeigt.
7. Ausführen des Programms
Aufruf des Befehls ERSTELLEN/AUSFÜHREN (von pr12272.exe) bzw. STRG+F5
Abb.: Ausführen einer Konsolenanwendung
26
Die Programmiersprache C++
Die Meldung „Press any key to continue“ wird von der Visual C++ - IDE hinzugefügt. Sie verhindert,
daß das Konsolenfenster gleich verschwindet.
Zum Schließen des Fensters drücke eine beliebige Taste.
3. Dev-C++
GCC-Compiler27, "Win32 native executables" für Konsole und grafische
Benutzeroberflächen, auch DLLs und statische Bibliotheken werden durch das "GNU
General Public License" – Produkt Dev-C++ mit einer IDE (Integrated Development
Environment) unterstützt. Dev-C++ läuft erfolgreich unter Windows 2000 bzw. XP.
Nach dem Aufruf erscheint folgendes Fenster:
Abb.:
Ausgangspunkt für Programmentwicklung ist der Menüpunkt FILE | OPEN
PROJECT OR FILE. Danach können die Quellen für das Programm ausgewählt
werden. Über die Kartenreiter werden die Quellen im Editor der IDE angezeigt.
Das Kompilieren, Binden besteht aus dem
- Präprozessor, z.B. zum Prüfen und Ersetzen der Makros und include-Dateien.
- Compiler zur Transformation des Quellcodes in Assembler
- Assembler zum Erzeugen von Maschinencode (binary object code)
- Linker zum Zusammenstellen von Object-Code zu einer ausführbaren Datei.
27
Dev-C++ kann auch in Kombination mit cygwin oder anderen GCC-Übersetzern aufgerufen werden.
27
Die Programmiersprache C++
Abb.
Das Kompilieren wird ausgelöst über den Menüpunkt EXECUTE (Einfügen) und Klick
auf COMPILE. Daraufhin werden Compiler und Linker automatisch aufgerufen. Im
unteren Bereich des von der IDE bereitgestellten Fensters wird der
Übersetungsvorgang protokolliert bzw. eine Liste mit Fehlern angezeigt.
Durch Klick auf RUN im Menü EXECUTE läuft das Programm im Rahmen eines
Textbildschirms ab:
Abb.: Textbildschirm zum Programm pr12282.cpp
28
Die Programmiersprache C++
In Dev-C++ kann ein Projekt zur Verwaltung mehrerer Quelldateien herangezogen
werden.
Es gibt unterschiedliche Projekttypen:
- Windows Application (Windows-Programm, das WIN32 API verwendet
- Console Application
- Static Library (erzeugt ein leeres Projekt und die Option, die der Linker zum Aufbau einer statischen
Bibliothek benötigt)
- DLL (erzeugt eine WIN32 Dynamic Library)
29
Die Programmiersprache C++
1.2.2.8 Visual C++ .NET
Visual Studio .NET (für ASP28-Webanwendungen und Desktopanwendungen
umfasst Entwicklungstools für Visual C++, Visual Basic und Visual C#, unterstützt
das .NET Framework29 über Common Language Runtime (CLR) und stellt
vereinheitlichte Programmierklassen bereit. Es enthält die MSDN (Microsoft
Developer Network) – Library (Dokumentations-Sammlung für Entwicklungstools).
Sowohl für Visual C++ .NET als auch Visual Basic .NET bzw. Visual C# .NET 30 kann
die
gleiche
IDE
(Integrated
Development
Environment,
Integrierte
Entwicklungsumgebung) verwendet werden.
Projekte anlegen und verwalten
Visual Studio ordnet den Quellcode in Projektmappen an. Das sind Ordner, in denen
eine oder mehrere Projekte gespeichert werden. Beim Erzeugen eines neuen
Projekts, wird automatisch eine neue Projektmappe erstellt. Die Projektmappe dient
dazu, Einstellungen, die für mehrere Projekte gelten, zusammen zu verwalten. Einer
Projektmappe können mehrere Projekte hinzugefügt werden. Am schnellsten lässt
sich ein Projekt über die Startseite anlegen.
28
Durch ASP .NET (aufgebaut auf der Programmierklasse des .NET Framework) wird ein
Webanwendungsmodell mit einem Satz von Steuerelementen sowie die Infrastruktur für die Erstellung von
Webanwendungen bereitgestellt.
29 Das Framework stellt den Entwicklern einen vereinheitlichten, objektorientierten, hierarchischen und
erweiterten Satz an Klassenbibliotheken zur Verfügung. Durch die Entwicklung eines gemeinsamen Satzes an
APIs für alle Programmiersprachen wird über die Common Language Runtime sprachübergreifende Vererbung,
Fehlerbehandlung und Debuggen möglich.
30 Visual C# .NET (steht für "C Sharp") ist eine neue objektorientierte Programmiersprache, eine
Weiterentwicklung von C und C++.
30
Die Programmiersprache C++
Abb. Der Eröffnungsbildschirm von Visual Studio .NET
Projektmappen-Explorer. Er bildet den Ausgangspunkt für die Navigation zu den
verschiedenen Teilen der Entwicklungsprojekte. Diese Ansicht gestattet es, die Teile
einer Anwendung in drei verschiedenen Modi zu betrachten:
- die KLASSENANSICHT ermöglicht die Navigation und die Bearbeitung von Quellcode auf C++Klassenebene.
- die RESSOURCENANSICHT erlaubt das Aufsuchen und Bearbeiten verschiedener Ressourcen in
der Anwendung. Dazu gehören Entwürfe von Dialogfenstern, Symbolen und Menüs. Diese Ansicht ist
nur verfügbar, wenn ein Projekt geöffnet ist.
- Die PROJEKTMAPPEN-EXPLORER-ANSICHT (Beschriftung: Projektmappen-Explorer) bietet eine
Übersicht über die Dateien, aus denen die Anwendung besteht. Man kann Dateien anlegen und in
ihnen navigieren. Da auch mehrere Projekte gleichzeitig geöffnet sein können, kann innerhalb des
Projektmappen-Explorer zwischen Projekten navigiert werden.
Die anfängliche Sammlung von Ansichten nach der Installation von Visual .NET ist
nur die Standard-Anordnung. Es ist möglich, die ursprünglichen drei Ansichten in
jedem beliebigem Bereich der IDE zu ziehen und der Gruppierung selbst neue
Ansichten hinzuzufügen.
Der Ausgabebereich. Nach dem Kompilieren der ersten Anwendung erscheint der
Ausgabebereich am unteren Rand der Visual-Umgebung und bleibt bis zum
Schließen geöffnet. Im Ausgabebereich stellt die IDE von Visual Studio alle
relevanten Informationen bereit. Hier kann der Fortschritt (Warnungen,
Fehlermeldungen) beim Kompilieren verfolgt werden. Auch der Debugger von Visual
C++ zeigt im Ausgabebereich alle Variablen und ihre aktuellen Werte an,, wenn man
den Code schrittweise bearbeitet.
31
Die Programmiersprache C++
Der Editorbereich umfaßt die Beschreibung der Entwicklungsumgebung, der nicht
anderweitig von Ansichten, Menüs oder Symbolleisten eingenommen wird. Hier
erscheinen die Fenster des Quellcode-Editors, falls C++-Quellcode bearbeitet wird.
Hier wird der Dialog-Designer angezeigt, falls ein Dialogfeld entworfen wird. Der
Editorbereich dient auch dem Symbol-Editor zur Anzeige, wenn man Symbole für
den Einsatz in den Anwendungen gestattet.
Menüleisten. Beim ersten Start von Visual C++ erscheinen unterhalb der Menüleiste
zwei Symbolleisten:
- Die Standardsymbolleiste enthält die meisten Standardwerkzeuge für das Öffnen und Speichern von
Dateien, Ausschneiden, Kopieren, Einfügen und verschiedene allgemeine, gebräuchliche Befehle.
- Die andere Symbolleiste enthält plausible Funktionen für Aktivitäten im Editorbereich.
Weitere Symbolleisten sind verfügbar.
Programmentwicklung
Programmentwicklung im Visual Studio ist organisiert in Lösungen, die ein oder
mehrere Projekte umfassen. Zu Beginn der Programmentwicklung steht deshalb das
Erstellen eines neuen Projekts.
Auf der Startseite befinden sich neben Hyperlinks auf bereits erstellte Projekte auch
eine Schaltfläche NEUES PROJEKT, über die ein neues Projekt angelegt werden
kann. Ein andere Möglichkeit besteht in der Wahl des Menüpunkts FILE | NEW |
PROJECT.
1. Erstellen einer prozedurorientierten C/C++-Konsolenanwendung
Aufgabenstellung. In C++ enthält die Standard Template Library ein mit einem CArray vergleichbaren Container mit dem Namen "vector". STL-Container sind als
Template-Klassen implementiert. Die folgende Konsolenanwendung soll die Vorteile
der STL-Container-Klasse "vector" gegenüber einem C-Array zeigen. Zur
Demonstration der Arbeitsweise von "vector", soll ein prozedurorientiertes C++Programm ganze Zahlen einlesen, sie sortieren und anschließend ausgeben.
Lösung. Die Lösung soll im Rahmen einer Konsolenanwendung durch ein Programm
in Visual C++.NET erfolgen.
1. Schritt: Start eines neuen Projekts.
Ausgangspunkt ist der Eröffnungsbildschirm von Visual Studio .NET. Mit DATEI | NEU und dem
Untermenü PROJEKT eröffne das Dialogfeld NEUES PROJEKT. In diesem Dialogfelkd links unter
PROJEKTTYPEN die Option VISUAL C++-Projekte und rechts unter VORLAGEN die Option WIN32Projekt wählen. Nach Eingabe des Namens vom neuen Projekt und Wahl des Speicherorts für die
Dateien des Projekts Klick auf OK.
32
Die Programmiersprache C++
Abb.: Das Dialogfeld Neues Projekt zum Erstellen einer prozedurorientierten Anwendung
2. Schritt.
Das WIN32-Anwendungs-Assistent-Dialogfeld wird als Nächstes (nach dem OK im Dialogfeld Neues
Projekt) geöffnet.
Abb.: Win32-Anwendungs-Assistent-Dialogfeld
33
Die Programmiersprache C++
Öffnen der Seite ANWENDUNGSEINSTELLUNGEN, danach Aktivieren unter ANWENDUNGSTYP
Konsolenanwendung mit der zusätzlichen Option Leeres Projekt:
Abb.: Die Seite Anwendungseinstellungen des Win32 Anwendungs-Assistenten
Nach dem Klick auf FERTIG STELLEN erstellt der WIN32-ANWENDUNGS-ASSISTENT automatisch
eine Reihe von Dateien. Es fehlt aber eine Datei, die den C-Quelltext ("Source") aufnimmt.
3. Schritt.
Über den Projektmappen-Explorer, Klick mit der rechten Maustaste auf Quelldateien. Über
Hinzufügen->Neues Element hinzufügen wird eine Quelldatei mit Hilfe des Fensters "Neues Element
hinzufügen" ausgewählt
34
Die Programmiersprache C++
Abb.
Als Template für die neue Quelle wird C++-Datei gewählt.
4. Schritt.
Nach dem Klick auf ÖFFNEN im Dialogfeld "Neues Element hinzufügen" öffnet sich die neue (leere)
Datei im Editor-Fenster.
5. Schritt.
Mit Hilfe des eingebauten Editors wird folgender Quelltext eingegeben:
#include <stdio.h>
int main(void)
{
printf("Hallo Welt\n");
return 0;
}
Der angegebene Quelltext hat mit der gewünschten Lösung nichts zu tun, kann aber erfolgreich
übersetzt und zum Laufen gebracht werden.
6. Schritt: Übersetzen zu einem lauffähigen Programm (Build bzw. Erstellen)
Mit ERSTELLEN | KOMPILIEREN wird das aktuelle Projekt in ein ausführbares Programm übersetzt.
Bei dem Übersetzungsvorgang wird u.a. die syntaktische Korrektheit des Programms überprüft. Treten
dabei Fehler auf, werden sie im Build-Fenster (links unten) angezeigt. Solange das Programm nicht
korrekt übersetzbar ist, kann es auch nicht ausgeführt werden.
35
Die Programmiersprache C++
Abb.
7. Schritt: Starten (ausführen) des Programms
Wenn das Programm erfolgreich übersetzt ist, kann es u.a. mit DEBUGGEN | STARTEN OHNE
DEBUGGEN ausgeführt werden. Es wird das folgende Konsolen-Fenster erzeugt:
Abb.
36
Die Programmiersprache C++
8. Schritt. Lösung der eigentlich gestellten Aufgabe mit einem C-Array und der in stdlib
vorhandenen Funktion qsort()31.
Die vorliegende Quelle wird mit folgendem Quelltext überschrieben:
#include <stdlib.h>
#include <iostream.h>
// a und b zeigen auf zwei Ganzzahlen. cmp gibt -1 zurueck,
// wenn a kleiner als b ist, 0 bei Gleichheit und 1, wenn
// a groesser als b ist
inline int cmp(const void *a, const void *b)
{
int aa = *(int *) a;
int bb = *(int *) b;
return (aa < bb) ? -1 :(aa > bb) ? 1 : 0;
}
// Einlesen einer Liste ganzer Zehlen von der Standard-Eingabe
// Sortieren mit qsort() aus der C-Bibliothek
// Ausgabe der Liste
int main(void)
{
const int size = 10; // Array mit max. 10 Elementen
int array[size];
int n = 0;
// Einlesen einer ganzen Zahl in das jeweilige n+1.te Element
while (cin >> array[n++]);
n--; // Einmal wurde zuviel gezaehlt
qsort(array, n, sizeof(int), cmp);
for (int i = 0; i < n; i++)
cout << array[i] << " ";
cout << endl;
return 0;
}
Anstatt #include <stdio.h> wurde hier #include <iostream.h> geschrieben. Das C-Header
stdio.h enthält C-spezifische Funktionen zur Ausführung standardmäßiger I/O-Operationen in C.
Durch den Wechsel von stdio.h in iostream.h werden in C++-spezifische Definitionen
eingebunden. Die Extension ".h" zeigt, dass die Anweisung die Programmsyntax alten Stils
verwendet.
Der vorliegende Quelltext führt nach Übersetzen und Ausführen zu einer korrekten Lösung.
9. Schritt. 2.Version mit STL-Komponenten.
Die STL besteht aus drei separaten Komponenten: Container, Algorithmen, Iteratoren. Zur Lösung der
Aufgabe sollen geeignete Komponenten ausgewählt werden. Ein geeigneter Container ist "vector"32.
Ein Algorithmus definiert eine Verhaltensweise eines Containers oder wendet eine gewisse Funtion
(sort())33 auf einen Container an, um dessen Inhalt zu be- oder zu verarbeiten. In der STL werden
Algorithmen durch Template-Funktionen repräsentiert. Sie können nicht nur mit STL-Containern,
sondern auch mit gewöhnlichen C-Arrays oder anderen anwendungsspezifischen Containern
verwendet werden. Die Interaktion zwischen Containertyp und dem Verhaltem der im Container
gespeicherten Daten steuern Iteratoren34. Sie ähneln Zeigern, mit denen auf Datenelemente
zugegriffen wird. In der STL wird ein Iterator-Objekt durch eine Iterator-Klasse repräsentiert. Mit dem
Operator "++" kann ein Iterator inkrementiert werden, mit dem Operator "*" kann auf ein einzelnes
Member des ausgewählten Elements zugegriffen werden. Es gibt verschiedene Klassen von
Iteratoren, die mit speziellen Containertypen verwendet werden. Sollen Elemente eines Containers
durchlaufen werden, verwendet man einen Iterator.
Die Quelle wird durch folgenden Quelltext überschrieben:
31
vgl. 5.1.1
vgl. 2.2.3.2 bzw. 5.2.8
33 vgl. 5.4
34 vgl. 5.3
32
37
Die Programmiersprache C++
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
// Einlesen einer Liste ganzer Zahlen von der Standard-Eingabe
// Sortieren mit dem STL-Algorithmus sort() aus der STL
// Ausgabe der Liste
int main(void)
{
vector<int> v; // erzeugt einen leeren vector
/* Eingabe */
int eingabe;
while (cin >> eingabe)
// Solange nicht end of file
v.push_back(eingabe); // Anhaengen an den vector
/* Verarbeitung */
sort(v.begin(),v.end());
// Der STL-Algorithmus sort() nimmt 2 Random-Access-Iteratoren
// und sortiert die dazwischen liegenden Elemente
/* Ausgabe */
int n = v.size();
for (int i = 0; i < n; i++)
cout << v[i] << " ";
cout << endl;
return 0;
}
Die neue Quelltextversion enthält die Anweisung using namespace std. Diese Anweisung ist eine
ANSI/ISO-C++-Anforderung an den Code. Das C++-Schlüsselwort using legt den Gültigkeitsbereich
von Bezeichnern fest. Zur Vermeidung von Namenskonflikten, kann der Programmierer Definitionen in
Namespaces einkapseln. Alle Namen, die zu Standard-C++ gehören, werden durch den Namespace
mit dem standardisierten Namen std geschützt. Die using-Anweisung funktioniert logischerweise wie
eine #include-Anweisung, indem sie die Definitionen einbindet, so dass die Namen in diesem
Programm gültig sind.
Die #include-Dateinamen wurden ohne die Extension ".h" angegeben. Der Dateiname ohne die
Dateierweiterung .h zeigt an, dass die neuen ANSI/ISO-C++-Anforderungen an den Code erfüllt sind.
Von C übernommene Header werden auch ohne die Extension ".h", aber durch Voranstellen des
Buchstabens "c" angegeben, z.B. "cstdio" ersetzt "stdio.h".
10. Schritt: Auf- und absteigende Sortierung
Neben Containern, Algorithmen und Iteratoren liefert die STL u.a.a Funktionsobjekte. Im Header
functional sind Klassen definiert35, die zum Erzeugen diverser Funktionsobjekte dienen. sort()
kann als 3. Parameter ein derartiges Funktionsobjekt aufnehmen.
Die Quelle wird zur auf- und absteigenden Sortierung mit folgendem Quelltext überschrieben.
#include
#include
#include
#include
<vector>
<algorithm>
<functional>
<iostream>
using namespace std;
// Einlesen einer Liste ganzer Zahlen von der Standard-Eingabe
// Sortieren mit dem STL-Algorithmus sort() aus der STL
// Ausgabe der Liste
int main(void)
{
vector<int> v;
35
// erzeugt einen leeren vector
vgl. 5.1.2.2
38
Die Programmiersprache C++
/* Eingabe */
int eingabe;
cout << "Gib ganze Zahlen ein, ";
cout << "Return nach jeder Zahl, <CTRL-Z> am Ende der Eingabe.\n";
while (cin >> eingabe)
// Solange nicht end of file
v.push_back(eingabe); // Anhaengen an den vector
/* Verarbeitung */
cout << "Aufsteigende Sortierung.\n";
sort(v.begin(),v.end());
// Der STL-Algorithmus sort() nimmt 2 Random-Access-Iteratoren
// und sortiert die dazwischen liegenden Elemente
/* Ausgabe der aufsteigend sortierten Ganzzahlen */
for (vector<int>::const_iterator viter = v.begin();
viter != v.end(); viter++)
cout << *viter << " ";
cout << endl;
/* Aufsteigend sortierte Elemente im vector absteigend sortieren */
sort(v.begin(),v.end(),greater<int>());
/* Ausgabe der absteigend sortierten ganzen Zahlen */
cout << "Absteigende Sortierung.\n";
for (vector<int>::const_iterator viter = v.begin();
viter != v.end(); viter++)
cout << *viter << " ";
cout << endl;
return 0;
}
Nach erfolgreichem Kompilieren erzeugt das Programm die folgende Ausgabe in einem
Konsolenfenster.
Abb.
39
Die Programmiersprache C++
2. Erstellen einer prozedurorientierten C/C++-Windows-Anwendung
Grundlagen von Windows. Drei Eigenschaften der Windows-Umgebung sind für den Programmierer
von Windows-Anwendungen von besonderem Interesse:
-
Windows arbeitet mit grafischen Benutzeroberflächen
Windows-Anwendungen werden aus Sicht des Benutzers in Fenstern ausgeführt.
Windows arbeitet ereignisorientiert
Aufgabe: Erstellen einer prozedurorientierten Anwendung mit dem Win32 Anwendungs-Assistenten
zur Bearbeitung von WM_PAINT-Meldungen.
Lösung. Sie umfasst folgende Schritte:
1. Öffne das Dialogfeld NEUES PROJEKT (DATEI | NEU | PROJECT)
Abb. Das Dialogfeld Neues Projekt zum Erstellen einer prozedurorientierten Windows-Anwendung
2. Im Dialogfeld NEUES PROJEKT links unter PROJEKTTYPEN die Option VISUAL C++-PROJEKTE
und rechts unter VORLAGEN die Option WIN32-PROJEKT wählen.
3. Eingabe von Name und Speicherort des neuen Projekts.
4. Klick auf OK.
Das WIN32 ANWENDUNGS-ASSISTENT-Dialogfeld wird geöffnet.
40
Die Programmiersprache C++
Abb. Das Win32 Anwendungs-Assistent-Dialogfeld
5. Öffne die Seite ANWENDUNGSEINSTELLUNGEN, danach unter ANWENDUNGSTYP die Option
WINDOWS-ANWENDUNG. Nach dem Klick auf FERTIG STELLEN erstellt der WIN32ANWENDUNGS-ASSISTENT automatisch eine Reihe von Dateien, einschl. einer Quelldatei. Diese
Quelldatei kann als Vorlage verwendet werden.
41
Die Programmiersprache C++
Abb.: Die Quelldatei des Projekts
Falls das Programm kompiliert und ausgeführt wird, erscheint folgendes einfaches Fenster.
42
Die Programmiersprache C++
Abb.: Das Standardfenster des Projekts
6. Einfügen einfacher Grafiken in den Arbeitsbereich des Projekt-Standardfensters.
6.1 Aufsuchen des folgenden Code in der Quelldatei
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Fügen Sie hier den Zeichnungscode hinzu...
EndPaint(hWnd, &ps);
break;
Verändere das Code-Fragment folgendermaßen:
/* Zeichnet 2 Linien und etwas Text in den Arbeitsbereich */
MoveToEx(hdc,0,0,NULL);
LineTo(hdc,639,429);
MoveToEx(hdc,300,0,NULL);
LineTo(hdc,50,250);
TextOut(hdc,120,30,"<- einige Linien ->",19);
6.2 Überesetzung und Start des ablauffähigen Programms
Es werden in das Fenster zwei Linien gezeichnet.
6.3 Das Graphic Device Interface
Für das Zeichnen von Grafik und Text unter Windows steht eine leistungsfähige Bibliothek bereit – das
Graphic Device Interface (GDI). Die GDI ist hardwareunabhängig. Ein Kreis wird sowohl auf dem
Bildschirm als auch auf dem Drucker mit gleichen Befehlen gezeichnet. Die Unterscheidung erfolgt
durch den Gerätekontext (Device Context – definiert in der Datenstruktur HDC (Handle to Devive
Context).
Das Zeichnen auf den Bildschirm (oder einen Drucker) stellt einen wichtigen Aspekt von WindowsProgrammen dar. Windows stellt dafür einen Satz von geräteunabhängigen Funktionen zur Verfügung,
die über entsprechende Treiber oder Bibliotheken die Hardware bedienen. Von den Gerätetreibern
werden Information zu 5 logischen Zeichenobjekten umgesetzt:
- Stift (zum Zeichnen von Geraden, Linien)
- Pinsel (zum Füllen von Bereichen)
- Schriftarten (zur Anzeige von Text)
- logische Farben (zur Beschreibung von Farben)
Der Gerätekontext ist die Verbindung zwischen einem Windows-Programm und dem Gerätetreiber.
Bevor Grafiken ausgegeben werden, wird zunächst der entsprechende Kontext angefordert. Damit
erhält man nicht nur rinr Freigabe zum Ausgeben auf einem Gerät, sondern auch eine Kennung
(Handle), die den GDI-Funktionen mitteilt, wo die grafische Ausgabe zu erfolgen hat.
Ein wichtiger datentyp ist daher der Handle auf einen Gerätekontext (Handle to Device Context). Er
schafft die Verbindung zwischen einem grafischen Befehl unter Windows und dem Ausgabegerät. In
jedem Gerätekontext sind etwa 20 Zeichenattribute gespeichert.
Die Koordination der Ausgabe wird vom Windows-Manager übernommen. Die Clipping36Informationen werden dabei in einer Datenstruktur kombiniert, die man Clipping-Bereich nennt. Es gibt
3 Window-Manager-Routinen bzgl. Clipping, die zum Erhalt eines Gerätekontexts dienen:
-
BeginPaint, EndPaint:Clippen des ungültigen Teils des Client-Bereichs. Dies dient der "Reperatur"
eines Fensters, dessen Notwendigkeit Windows mit der Meldung WM_PAINT anzeigt.
GetDC, ReleaseDC: Clippen des ungültigen Teils des Client-Bereichs. Dies dient der "Reperatur"
eines Fensters, deren Notwendigkeit Windows mit der Meldung WM_PAINT anzeigt.
GetWindowDC, ReleaseDC: Clippen des gesamten Fensters, Client- und Nicht-Client-Bereiche.
Dies wird jedoch normalerweise von der Standard-Fensterprozedur übernommen.
6.4 Funktionen für das Zeichnen von Grafik und Text unter Windows
36
Unter Clipping versteht man das Trennen der Bildschirm-Ausgaben verschiedener Programme. Es ähnelt dem
Erstellen imaginärer Zäune um den Zeichenbereich eines Programms. Daten können dann nicht außerhalb dieses
Zaunes ausgegeben werden.
43
Die Programmiersprache C++
Text. Für die Ausgabe von Text stehen 5 Routinen zur Verfügung: DrawText, ExtTextOut, GrayString,
TabbedTextOut und TextOut
TextOut(hDC,x,y,lpString,nCount)
Linien. Jede Linie besitzt einen Start- und Endpunkt, es wird der Inklusiv-Exklusiv-Algorithmus
verwendet, d.h.: Der Stratpunkt gehört zur Linie, der Endpunkt jedoch nicht. Das GDI verfügt über eine
Reihe von Funtionen zur Drstellung von Linien und Kurven.
MoveToEx(hdc,x,y,&pt)
LineTo(hdc,x,y)
Rectangle(hdc,x1,y1,x2,y2)
Ellipse(hdc,x1,y1,x2,y2)
Chord(hdc,x1,y2,x2,y2,x3,y3,x4,y4)
Pie(hdc, x1,y2,x2,y2,x3,y3,x4,y4)
TextOut(hdc,x,y,"text",count)
setzt die Zeichenposition und speichert die alte in 'pt'
malt eine Linie von der aktuellen Zeichenposition zu dem
angegebenen Punkt
zeichnet ein Rechteck, der durch die Koordinaten x1, y1, und
x2,y2 beschrieben wird. Alle Parameter außer dem Handle
hdc sind vom Typ int
Das Handle für den Gerätekontext ist durch hdc gegeben. Alle
anderen Parameter sind vom Typ int. Zurückgegeben wird ein
Wert vom Typ bool.
Damit kann eine Linie zwischen 2 Punkten x3, y3 und x4,y4
gezeichnet werden.
Von jedem der Punkte (x3,y3) bzw. (x4,y4) werden zwei Linien
zum Mittelpunkt des den Kreis bzw. die Ellipse
umschreibenden Rechtecks gezeichnet.
zeichnet einen Text auf den DC
Abb.: Wichtige Zeichenfunktionen der GDI
Ergänze den angegeben Quellcode zum Zeichnen von zwei Linien folgendermaßen
/* Zeichnen einer Ellipse */
Ellipse(hdc,550,100,625,150);
TextOut(hdc,440,115,"eine Ellipse ->",15);
/* Zeichnen eines Bogensegments in den Arbeitsbereich */
Chord(hdc,550,20,630,80,555,25,625,70);
TextOut(hdc,410,30,"ein Bogensegment ->",19);
/* Zeichnen eines Kreissegments */
Pie(hdc,300,50,400,150,300,50,300,100);
TextOut(hdc,350,70,"<- Ein Kreissegment ->",19);
/* Zeichnen eines Rechtecks */
Rectangle(hdc,500,200,600,300);
TextOut(hdc,610,250,"<- Ein Rechteck",15);
Nach Übersetzen und Start zeigt das Fenster die Wirkungsweise der grafischen Grundfunktionen.
6.5 Farb- und Mustereinstellungen bei Zeichenfunktionen
Die Zeichenfunktionen enthalten keine Parameter für Farbe und Stil. Aus Effizienzgründen sind diese
Parameter global einzustellen.
Die Füllfarbe wird definiert mit einem Pinsel (Brush), z.B.:
HBrush hBrush = CreateSolidBrush(RGB(0,255,0); // oder Farbkonstanten
Die Linieneigenschaften werden definiert mit einem Stift (Pen), z.B.:
HPen hPen = CreatePen(Style,Width,cl);
mit Style = PS_SOLID oder PS_DASH oder PS_DOT oder Kombinationen.
Sowohl Pinsel als auch Stift müssen dem GDC bekannt gemacht werden und sollten nach Gebrauch
wieder auf den alten Wert rückgesetzt werden, z.B.:
HRBRUSH hOldBrush = (HBRUSH) SelectObject(hdc,hBrush);
/* --- Zeichenoperationen ---*/
DeleteObject(SelectObject(hdc,hOldBrush));
hOldBrush = NULL;
44
Die Programmiersprache C++
3. Verwaltete C++-Anwendung
Mit verwaltetem C++ wird eine Anwendung für die Common Language Runtime
(CLR) erstellt. Die CLR entspricht grundsätzlich der Virtual Machine von Java. Es
handelt sich um eine binäre Maschinensprache für einen Prozessor, der nicht
wirklich existiert. Der Zweck der CLR ist, Anwendungen für die CLR anstatt für einen
bestimmten Computerprozessor zu kompilieren. Der Vorteil dabei ist, dass nun jede
Anwendung auf jeder Hardware laufen kann, für die es eine Version der CLR gibt.
C und C++ sind deshalb so beliebt, weil sie sich direkt in die Maschinensprache
kompilieren lassen. Das widerspricht aber dem Konzept, das der CLR zugrunde liegt.
Mit verwaltetem C++ wird eine Anwendung für die CLR kompiliert anstatt für native
Maschinensprache.
Ein Vorteil bei der Erstellung verwalteter C++-Anwendungen ist die eingebaute
Objekt-Bibliothek in der .NET-Plattform.
Die Präprozessor-Direktive #using kann DLLs und eigens für die CLR kompilierte
Objekte importieren, künftig auch ausführbare Dateien, die für die CLR kompiliert
wurden. Die wichtigste DLL ist mscorlib.dll, die viele Core-Objekte enthält, die
Teil der .NET-Plattform sind. Die Direktive steht am Anfang des Quellcodes:
#using <mscorlib.dll>
Die Direktive using namespace ist Teil der Sprachspezifikation in C++. Sie
ermöglicht die Festlegung eines Namespace und verschachtelter NamespaceBezeichner. Objekte aus dem Namespace können ohne vollständig qualifizierten
Namen der Objekte verwendet werden.
Arbeitsschritte für ein Projekt mit verwaltetem C++
1. Schritt: Projektstart (Erzeugen eines neuen Projekts)
1. In der Visual Studio .NET –Umgebung wähle aus dem Menü FILE | NEW | PROJECT:
45
Die Programmiersprache C++
2. In dem vorliegenden Fenster wird auf linken Seite Visual C++-Projekte und auf der rechten Seite
"Verwaltete C++-Anwendung" gewählt.
3. Bestimme den Namen den Projekts und gib den Speicherort an, in dem das Projekt erstellt werden
soll. Den Rest übernimmt Visual Studio.
Visual Studio .NET erzeugt daraufhin eine Lösung mit einem einfachen Projekt. Das Projekt enthält
verschiedene Dateien (u.a.a. assemblyinfo.cpp und pr12281.cpp)
2. Schritt: Sündenfall "Hallo Welt!"
Quellcodemodifikationen.
1. Doppelklick auf die Datei pr12281.cpp im "Projektmappen-Explorer". Der "ProjektmappenExplorer" kann mit Hilfe des Menüpunkts "Ansicht | Projektmappen-Explorer" aufgerufen werden.
2. An einer für Veränderungen vorgesehenen Stelle der generierten Schablone trage ein:
Console::WriteLine("Hallo C++ .NET Welt!");
46
Die Programmiersprache C++
Kompilieren der Anwendung
1. Das Visual C++-Projekt kann kompiliert werden über den Menüpunkt Erstellen | pr12281
erstellen und gebunden werden.
2. Fehler und Nachrichten vom Compiler werden im Ausgabebereich der IDE angezeigt. Falls keine
Fehler vorliegen, kann die Windows-Anwendung über den Menüpunkt Debuggen | Starten ohne
Debugger ausgeführt werden.
Programmausgabe
47
Die Programmiersprache C++
3. Schritt: Die Programmstruktur.
"using"-Direktive. Das .NET Framework versorgt den Entwickler mit zahlreichen nützlichen Klassen.
Die Klasse Console ist für Ein-, Ausgaben des Konsolenfensters zuständig. Die Klassen sind in einer
hierarchischen Baumstruktur organisiert. Der vollqualifizierte Name der Klasse Console ist
System::Console. Andere Klassen derselben Gruppe sind bspw. System::IO::FileStream und
System::Collections::Queue. Die Direktive using namespace erlaubt das Referenzieren von Klassen
in dem Namensraum (namespace) ohne Angabe des vollqualifizierten Namens.
Die Anweisung #using <mscorlib.dll> wird in allen Visual C++ .NET Dateien verlangt, die die
Anwendung von verwaltetem Code fordern.
Die Funktion _tmain() übernimmt die Steuerung, nachdem die Anwendung in den Speicher geladen
ist.
4. Schritt: Eingabe über die Konsole.
Nötig ist eine Aufforderung zur Eingabe von Namen der Eingabe- und Ausgabedatei.
Quellcodemodifikationen:
// TODO: Ersetzen Sie den Beispielcode durch Ihren eigenen Code.
// Beschreibung der Programmfunktion
Console::WriteLine("QuickSort C++ .NET Anwendungsbeispiel\n");
// Aufforderung zur Eingabe durch den Benutzer
Console::Write("Quelle: ");
String* szSrcFile = Console::ReadLine();
Console::Write("Ausgabe: ");
String* szDestFile = Console::ReadLine();
// Erfolgreiche Rückkehr
return 0;
Einlesen von der Console: Die statische Funktion ReadLine der Klasse Console fordert den Anwender
zur Eingabe auf. Den eingegebenen String nimmt die Funktion zur Weitergabe als Rückgabewert auf.
Programmausgabe: Über Debuggen | Starten ohne Debuggen ergibt sich folgender Textbildschirm:
5. Schritt.
Das Programm muß die eingelesenen Zeilen mit den Zeichenkettet in einen Array einlesen. Dazu ist
eine .NET-Klasse nötig, die ein Array von Objekten verwalten kann.
Quellcodemodifikationen:
using namespace System::Collections;
ArrayList* szContents = new ArrayList();
Verwendung der KLasse ArrayList. Importiert wird der Namensraum (namespace)
System::Collections für den Direktbezug aus ArrayList. Diese Klasse implementiert einen
48
Die Programmiersprache C++
dynamisch erweiterbaren Array von Objekten. Zum Einfügen dient die Methode Add() der ArrayListKlasse, z.B.:
String* szElement = new String("fuege mich ein");
ArrayList* szArray = new ArrayList();
szArray->Add(szElement);
Die Wiedergewinnung eines gespeicherten Element erfolgt über die Angabe vom Index des
gewünschten Elements durch die Methode get_Item()37, z.B.:
Console::WriteLine(szArray->get_Item(2))
6. Schritt: Dateieingabe / Dateiausgabe
Jede Zeile aus der Eingabe wird in einem Zeichenketten-Array eingelesen. Danach wird der
Zeichenketten-Array ausgegeben. Anschießend wird der Quicksort-Algorithmus zum Sortieren
herangezogen.
Quellcodemodifikationen.
using namespace System::IO;
...
// Einlesen der Eingabedatei
String* szSrcLine;
ArrayList* szContents = new ArrayList();
FileStream* fsInput = new
FileStream(szSrcFile,FileMode::Open,FileAccess::Read);
StreamReader* srInput = new StreamReader(fsInput);
while (szSrcLine = srInput->ReadLine())
{ // Anhaengen im Array
szContents->Add(szSrcLine);
}
srInput->Close();
fsInput->Close();
//TODO: Aufruf des Quicksort
// Ausgabe der sortierten Zeilen
FileStream* fsOutput = new
FileStream(szDestFile,FileMode::Create,FileAccess::Write);
StreamWriter* srOutput = new StreamWriter(fsOutput);
for (int nIndex = 0; nIndex < szContents->Count; nIndex++)
{
// Schreibe eine Zeile in die Ausgabedatei
srOutput->WriteLine(dynamic_cast<String*>
(szContents->get_Item(nIndex)));
}
srOutput->Close();
fsOutput->Close();
Lesen aus der Eingabedatei. Die Klasse FileStreamReader wird zum Öffnen der Eingabedatei
benötigt. Der FileStreamReader wird ein StreamReader unterlegt, so dass die Methode
ReadLine() von StreamReader benutzt werden kann. Falls ReadLine() NULL zurückgibt, ist das
Ende der Datei erreicht.
Schreiben in die Ausgabedatei. Ein StreamWriter-Objekt wird an ein FileStream-Objekt angehängt.
Das ermöglicht die Anwendung der Methode WriteLine().
7. Schritt: Erzeugen einer Funktion, die den Quicksort auf einem Array von Zeichenketten
implementiert.
Quellcodemodifikationen.
37
Dokumentation in der MSDN-Library
49
Die Programmiersprache C++
4. Verwaltete C++-Anwendung mit grafischer Benutzeroberfläche und GDI+
(Dialoganwendung)
.NET stellt mit "WindowsForms" System.Windows.Forms, für die Programmierung
grafischer Benutzeroberflächen zur Verfügung. Zum Zeichnen gibt es GDI+, eine
Weiterentwicklung der Windows-API GDI. Voraussetzung ist verwalteter Code.
Windows Forms. Forms sind ein Standard-Window, ein MDI-Client, eine Dialogbox. Die Controls
(Steuerelemente) einer solchen Form sind wieder Forms.
Zeichnen mit .NET GDI+. Die Unterstützung zum Zeichnen erfolgt mit der "class library GDI+",
bereitgestellt über "Gdiplus.dll". GDI+ besteht aus: 2D Vector Graphic (Linien, Figuren, ...), Imaging
(Darstellung von Bildern), Typographie bzw. Textverarbeitung.
Soll in eine Form etwas gezeichnet werden, wird die OnPaint-Methode überschrieben.
GDI+ befindet sich in der Sammlung System.Drawing.dll. Alle Klassen befinden sich in den
"namespaces": System.Drawing, System.Text, System.Printing, System.Internal, System.Drawing2D
und System.Design. Die Klasse Graphics kapselt die Fläche zum Zeichnen. Zum Zeichnen von
irgendeinem Objekt (z.B. Kreis, Rechteck) ist ein Handle zum Zeichnen auf der Zeichenfläche über die
Klasse Graphics zu beschaffen (z.B. über den Parameter der Methode OnPaint). Mit einer "Graphics
Reference" können folgende Klassen zum Zeichnen verschiedener Objekte herangezogen werden:
DrawArc, DrawBeziew, DrawBeziers, DrawClosedCurve, DrawCurve, DrawEllipse, DrawImage,
DrawLine, DrawPath, DrawPolygon, DrawPie, DrawRectangle, DrawString, FillEllipse, FillPath, FillPie,
FillPolygon, FillRectangle, FillRegion.
Programmerstellung. Für eine Dialoganwendung wird eine eigene Klasse von der Basis Form
abgeleitet. Member dieser Klasse sind Textbox, Button und andere Controls (Steuerelemente). Die
Member werden im Konstruktor erzeugt, mit den wesentlichen Eigenschaften ausgestattet und als
Kinder der Hauptform zugeordnet. Den Events eines Controls, wie dem Klick eines Buttons, auf den
reagiert werden soll, wird eine Event-Funktion zugeordnet. Die Event-Funktion enthält den Code, der
abläuft, wenn der Button gedrückt wurde.
1. Schritt: Unter dem Projektnamen pr12283 wird eine verwaltete C++-Anwendung erstellt, die die
GDI+-Komponenten aufnehmen soll.
Visual Studio .NET erzeugt das Projekt mit verschiedenen Dateien (u.a.a. AssemblyInfo.cpp,
pr12283.cpp).
2. Schritt: Der vorliegende Quellcode (pr12283.cpp) wird überschrieben durch
#include "stdafx.h"
#using <mscorlib.dll>
#using <System.DLL>
#using <System.Windows.Forms.DLL>
#using <System.Drawing.DLL>
using namespace System;
using namespace System::Windows::Forms;
using namespace System::Drawing;
#include <tchar.h>
public __gc
{
private:
TextBox*
Button*
Button*
Button*
int
public:
Form1()
{
Size =
class Form1 : public Form
box;
closeButton;
testButton;
test1Button;
pensize;
System::Drawing::Size(400,400);
50
Die Programmiersprache C++
box = new TextBox;
box->Size
= System::Drawing::Size(100,100);
box->Location = System::Drawing::Point(10,10);
box->Text
= "- - - ";
closeButton = new Button;
closeButton->Location = System::Drawing::Point(10,120);
closeButton->Text
= "Close";
closeButton->Size
= System::Drawing::Size(70,20);
closeButton->Click
+= new EventHandler(this, CloseButtonClicked);
testButton = new Button;
testButton->Location = System::Drawing::Point(10,140);
testButton->Text
= "test Textbox";
testButton->Size
= System::Drawing::Size(70,20);
testButton->Click
+= new EventHandler(this, TestButtonClicked);
test1Button = new Button;
test1Button->Location = System::Drawing::Point(10,160);
test1Button->Text
= "test gdi+";
test1Button->Size
= System::Drawing::Size(70,20);
test1Button->Click
+= new EventHandler(this, Test1ButtonClicked);
pensize = 1;
this->Controls->Add(box);
this->Controls->Add(closeButton);
this->Controls->Add(testButton);
this->Controls->Add(test1Button);
}
private:
void CloseButtonClicked( Object* sender, EventArgs* e)
{
Close();
}
void TestButtonClicked( Object* sender, EventArgs* e)
{
box->Text = "hallo welt";
Width += 10;
Height -= 10;
Left += 30;
Top
+= 30;
testButton->Enabled = false;
}
void Test1ButtonClicked( Object* sender, EventArgs* e)
{
int x = 200;
int y = 10;
int w = 100;
int h = 90;
pensize++;
Rectangle r = System::Drawing::Rectangle(x, y, w, h);
Invalidate(r);
}
protected:
virtual void OnPaint(PaintEventArgs *e)
{
Pen *myPen;
int x = 200;
int y = 10;
int w = 100;
int h = 90;
// 200, 10 - 300, 100
Random *rdm1 = new Random((int)DateTime::Now.Ticks);
Color c
= Color::FromArgb(rdm1->Next(255), 0, 0, 255);
myPen = new Pen(c, pensize);
Graphics *myGraphics = e->Graphics;
myGraphics->DrawLine(myPen, x, y, x+w, y+h);
// 200, 110 - 300, 200
y = 110;
System::Drawing::Drawing2D::HatchBrush *
myHatchBrush = new System::Drawing::Drawing2D::HatchBrush(
51
Die Programmiersprache C++
System::Drawing::Drawing2D::HatchStyle::Cross,
Color::FromArgb(255, 0, 255, 0),
Color::FromArgb(255, 0, 0, 255));
myPen->Dispose();
myPen = new Pen(Color::FromArgb(255, 255, 0, 0), 1);
myGraphics->FillRectangle(myHatchBrush, x, y, w, h);
myGraphics->DrawRectangle(myPen, x, y, w, h);
// 200, 210 - 300, 300
y = 210;
System::Drawing::Drawing2D::LinearGradientBrush *
myGradBrush = new System::Drawing::Drawing2D::LinearGradientBrush(
Point( x,
y),
Point(x+w, y+h),
Color::FromArgb(255, 0, 255, 0),
Color::FromArgb(255, 0, 0, 255));
myGraphics->FillRectangle(myGradBrush, x, y, w, h);
}
};
// Dies ist der Einstiegspunkt für die Anwendung
int _tmain(void)
{
// TODO: Ersetzen Sie den Beispielcode durch Ihren eigenen Code.
// Instantiate a new instance of Form1.
Form1 *f1 = new Form1;
// Display a messagebox. This shows the application
// is running, yet there is nothing shown to the user.
// This is the point at which you customize your form.
System::Windows::Forms::MessageBox::Show("The application "
"is running now, but no forms have been shown.");
// Customize the form.
f1->Text = "Running Form";
// Show the instance of the form modally.
f1->ShowDialog();
return 0;
}
3. Schritt: Nach dem Erstellen von pr12283 und Start ohne Debuggen erscheint nach dem
Konsolenfenster und einer Message Box das folgende Fenster:
52
Die Programmiersprache C++
Nach einem Klick auf den Button "test TextBo" bzw. mehreren Klicks auf "test gdi+" verändert sich das
Fenster:
53
Die Programmiersprache C++
1.3 Deklaration und Definition von Bezeichnern
Allgemeine Form: typname bezeichner = anfangswert;
(Die Angabe des Anfangswerts ist optional)
bezeichner: Folge von Buchstaben und Ziffern, das 1. Zeichen ist Buchstabe, Großund Kleinbuchstaben werden unterschieden. Das spezielle Zeichen "_"
wird als Buchstabe gewertet. Selbstdefinierte Namen (ProgrammiererWörter) dürfen nicht mit den vordefinierten Schlüsselwörtern
übereinstimmen. Ein Name kann beliebig lang38 sein. Auch
Funktionsnamen unterliegen der angegebenen Konvention 39.
Definitionen reservieren Speicherplatz. Keine Definitionen sondern Deklarationen
sind bspw.:
extern int a;
typedef int ganzZ;
int f(int);
extern const a;
// Vereinbarung eines anderen Typnamens
In C++ muß eine Variable, bevor sie benutzt wird, definiert sein. Variable besitzen
einen Namen und erhalten einen Datentyp zugeordnet, z.B.:
char zeichen;
„char“ ist der Typ-Spezifizierer, „zeichen“ bestimmt ein Objekt, dem Speicherplatz
zugeordnet ist.
Zwei Werte sind mit symbolischen Variablen verknüpft:
-
38
39
Datenwert (r-value)
Speicheradresse (l-value) unter der der Datenwert gespeichert ist.
Manchmal ist die Länge begrenzt, z.B. 31 Zeichen.
Die Konventionen zeigen Regeln für die Struktur eines Bezeichners (Namens), auch Syntax genannt
54
Die Programmiersprache C++
1.4 Speicherklassen, Gültigkeitssbereiche und Namensbereiche
1.4.1 Speicherklassen
Jedes Objekt in C++ hat 2 Attribute: Eines bestimmt den Typ des Objekts und das
andere legt die Speicherklasse fest. Die Speicherklasse bestimmt die (zeitliche)
Lebensdauer und den (räumlichen) Geltungsbereich des Objekts. Hinsichtlich der
räumlichen Gültigkeit oder Sichtbarkeit unterscheidet man lokale und globale
Objekte.
So können die geschweiften Klammern in /* 19 */ bzw. /* 27 */ Anfang und
Ende eines Blocks bestimmen. Ein Block umfaßt mehrere sequentielle Anweisungen
zu einem Verbund zusammen und definiert einen Geltungsbereich für die in ihm
enthaltenen Variablen.
Generell sind Variable lokal, d.h. auf den Block (oder den lokalen Geltungsbereich)
beschränkt, in dem sie definiert wurden. In Ausnahmefällen kann der
Geltungsbereich von Variablen durch außerhalb von Blöcken befindliche Definitionen
erweitert werden40:
/* 14 */ int n;
/* 15 */ int x[20];
// globale Vereinbarung von Variablen
C++ kennt die Speicherklassen auto(matic) und static. Sie bestimmen die
Lebensdauer eines Objekts. Die Lebensdauer von Objekten der Speicherklasse
auto ist die Aktivitätszeit des Blocks, in dem sie definiert sind.
Eine „auto“-Variable ist der Standardfall für lokale Variable. „auto“-Variable haben
Priorität vor globalen Variablen. Sie verstellen dem Block praktisch die Sicht auf
übergeordnete Objekte des gleichen Namens, z.B.:
#include <iostream.h>
int x = 2;
int main()
{
cout << "\n" << x; // Ausgabe x = 2
{
int x = 3;
cout << "\n" << x; // Ausgabe x = 3
x = 9;
}
cout << "\n" << x;
}
Die Speicherklasse static gibt es in 2 unterschiedlichen Formen, je nachdem, ob es
sich um lokale oder globale Variable handelt: Setzt man das Schlüsselwort static vor
eine Deklaration, ändert man bei globalen Objekten die räumliche Gültigkeit, bei
lokalen Variablen verleiht man ihnen eine zeitliche Permanenz.
Man kann überall, wo „auto“-Variablen stehen können, durch das Schlüsselwort
static die Lebensdauer von Objekten auf die Dauer des Programms ausdehnen, die
räumliche Geltungsbereich bleibt allerdings lokal. Nur der Block, in dem sie definiert
40
vgl. PR12101.CPP
55
Die Programmiersprache C++
wurden, hat einen Zugriff auf diese Variablen. Statische lokale Größen behalten ihre
Werte in einem Block bei, z.B.:
void f()
{
static int aufrufe = 1;
cout << "Aufruf zum " << aufrufe << " .Mal\n";
aufrufe = aufrufe + 1; // sukzessives Inkrementieren bei jedem Aufruf
}
Globale Variable haben eine unbeschränkte Lebensdauer. Ihr Geltungsbereich
erstreckt sich über alle Programmteile. Die räumliche globale Gültigkeit läßt sich
durch das Schlüsselwort static einschränken. Sie sind außerhalb von der definierten
Quelldatei „unsichtbar“. Ein Zugriff auf diese Variablen führt zu einer Fehlermeldung
des Compilers.
„Objekte mit dynamischer Lebensdauer“ werden während eines Programms mit
besonderen Funktionsaufrufen angelegt und wieder freigegeben. Speicherzuweisung
erfolgt entweder über Standardbibliotheksfunktionen wie malloc oder mit dem C++Operator new. Der zugewiesene Speicherplatz liegt in einen bestimmten
Speicherbereich, dem sog. Heap. Mit free oder delete wird der Speicherplatz wieder
freigegeben.
Häufig wird der Datentyp „register“ zu den Speicherklassen gezählt. „register“ ist
jedoch keine eigene Speicherklasse. Das Schlüsselwort bewirkt, soweit es möglich
ist, die so typisierte Variable im Prozessorregister aufzubewahren. Auf die Variable
kann dann schneller zugegriffen werden. Die Anweisung „register“ ist für lokale
Variable und für „auto“-Variable zulässig.
1.4.2 Gültigkeitsbereiche bzw. Geltungsbereiche
In der Programmiersprache C gibt es zwei Arten von Gültigkeitsbereichen: dateiweit
(file scope) und blockweit (block scope). In C++ gibt es noch einen dritten
Güligkeitsbereich: klassenweit (class scope)41. In den neuen Versionen von C++
kommt noch ein vierter Gültigkeitsbereich hinzu: namensraumweit (namespace
scope), Objekte von C++ besitzen in der Regel vier Geltungsbereiche: lokal,
funktional, global, funktionslokal und klassenlokal.
Lokale Namen werden innerhalb eines Blocks vereinbart und sind von der Stelle
ihrer Vereinbarung bis zum Ende des Blocks bekannt (sichbar).
Bsp.:
int i = 1;
/* i ist ausserhalb des Blocks definiert, ist
innerhalb eines jeden anderen Blocks gueltig
und heisst deshalb globale Variable */
main()
{
41
Die in einer Klasse definierten Namen haben Gültigkeit nur innerhalb einer Klasse. Insbesondere gibt es
keinen Konflikt – und kein Overloading – zwischen freien Funktionen und Methoden. Der Scope Operator ::
kann verwendet werden, um auf Bezeichner zuzugreifen, die nur innerhalb einer Klasse definiert sind.
56
Die Programmiersprache C++
int i = 2; // überlagert äußeres i
{
double i = 3.14; //überlagert inneres int i
cout << i << "\n";
} // Ende des Gültigkeitsbereichs von double i
cout << i << "\n";
} // Ende des Gültigkeitsbereichs des inneren i
Globale Namen sind außerhalb jedes Blocks und außerhalb jeder Klasse deklariert 42.
Ihre Sichbarkeit reicht von der Stelle der Deklaration bis zum Ende der Quelldatei (,
falls nicht zwischendurch eine Überlagerung erfolgt).
C++ verfügt über den "::"-Operator (scope resolution operator). Er ermöglicht den
Zugriff auf die Datei eines globalen Namens, z.B.:
int i;
int main()
{
int i = 2;
{
// Beginn des inneren Gueltigkeitsbereichs
double i = 3.14;
cout << ::i << "\n"; // bezeichnet das äußere i
}
cout << i << "\n";
// Ende des inneren Gültigkeitsbereichs
}
Globale Namen können außerdem von getrennt übersetzten Programmteilen
referenziert werden, falls sie
a) nicht als static definiert sind
b) in jeder "Konsumentendatei" ordnungsgemäß deklariert sind (kann durch Angabe des
Schlüsselworts extern bei der Deklaration des globalen Namens in der Konsumentendatei
erreicht werden).
Jedes Objekt, das außerhalb jedes Blocks vereinbart wird, gehört zu Speicherklasse
extern. Das Objekt ist damit global und zeitlich permanent. Es hat eine Lebensdauer,
die sich über die gesamte Programmlaufzeit erstreckt und ist in allen zum Programm
zugehörigen Programmbausteinen (Moduln) verfügbar. Für Variable ist die
Verfügbarkeit erst ab dem Deklarationszeitpunkt gegeben. Funktionen gehören
immer der Speicherklasse extern an, da eine Funktion nicht innerhalb eines Blocks
definiert werden kann. Der Compiler betrachtet den Funktionstyp als extern und
unterstellt den Typ int. Ein externes Objekt darf in allen Programmbausteinen nur
einmal auftreten43.
„extern“-Variablen werden in einem anderen Quelltext definiert. Sie werden durch
eine „extern“-Deklaration zugänglich gemacht.
Eine weitere Möglichkeit zur Schaffung eines Sichbarkeitsbereiches sind
Namensräume (namespaces). Wird bspw. Der zur C++-Standardbibliothek
gehörende Namensraum „std“ benutzt, dann kann das über
using namespace std;
angegeben werden. Namensräume spielen bei der Benutzung verschiedener
Bibliotheken eine Rolle.
42
43
vgl. Zeilen /* 14 */ und /* 15 */ in PR12101.CPP
Der Compiler stellt den Fehler nicht fest, wohl aber der Linker mit „duplicate global“
57
Die Programmiersprache C++
1.4.3 Externe Variable und Funktionen
Funktionen können in vielen unterschiedlichen Programmen benutzt werden. Es
wäre umständlich, jedesmal den Quelltext einer Funktion in ein neu erstelltes
Programm hineinzukopieren. C++ benutzt dafür den Mechanismus der getrennten
Übersetzung.
Bsp.: Eine Funktion zum Potenzieren ist in einem separaten Block abzulegen und
immer, wenn man 2 Zahlen potenzieren möchte, hinzuzubinden.
Die erste Datei hat folgendes Aussehen44
double Potenz(double dX, short n)
{
// Potenz() potenziert dX und n.
double dPot = 1.0;
for(short i = 1; i <= n; i++)
dPot = dPot * dX;
return(dPot);
// Rueckgabewert!
}
// Ende von Potenz()
Die zweite Datei hat folgendes Aussehen45:
#include <iostream.h>
int main()
{
short exponent;
double dBasis, dPot;
// Extern Deklaration
extern double Potenz(double, short = 2);
void eingabe(double &, short &),
ausgabe(double, short, double);
eingabe(dBasis, exponent);
// Aufruf der externen Funktion!
dPot = Potenz(dBasis,exponent);
ausgabe(dBasis, exponent, dPot);
} // Ende von main()
void eingabe(double &dB, short &e)
{
// Hier werden Basis und Exponent eingelesen.
cout << "\n\tBasis: ";
cin >> dB;
do{
}
cout << "\tExponent (> 0): ";
cin >> e;
} while(e < 0);
// Ende von Eingabe()
void ausgabe(double dB, short e, double dErg)
{
// Und hier findet die Ausgabe statt.
cout << "\n\tErgebnis: " << dB << " hoch ";
cout << e << " = " << dErg << '\n';
} // Ende von Ausgabe()
44
45
PR14203.CPP
PR14205.CPP
58
Die Programmiersprache C++
Die Compilierung der beiden separaten Dateien erfolgt getrennt, die
Zusammenstellung zu einem ablauffähigen Programm erfolgt über ein sog. Projekt in
Borland C++ bzw. über ein „Makefile“ in GNU C/C++.
Ergänzen von Namen: Falls ein C++-Baustein kompiliert ist, erzeugt der Compiler
Funktionsnamen, die die Typen der Funktionsargumente in verschlüsselter Form
enthalten (Namensergänzung, name mangling). Es gibt Fälle, in denen die
Namensergänzung nicht gewünscht ist. Das wird dem Compiler auf folgende Weise
mitgeteilt:
extern "C" void exit(int);
Man kann die Deklaration extern "C" auch auf einen Namensblock anwenden:
extern "C"
{
void Cfunktion_1(int)
void Cfunktion_2(int);
void Cfunktion_3(int) }
1.4.4 Namensbereiche
Namensbereiche (name spaces), auch Namensräume genannt, dienen dazu, die
Wahrscheinlichkeit von Namenskollisionen (name clashes) zu verringern, indem man
neben dem Gültigkeitsbereichen global, lokal und klassenweit noch zusätzlich den
Gültigkeitsbereich Namensbereich einführt.
Deklaration eines Namensbereichs
Ein Namensbereich beispielspace wird so deklariert:
namespace beispielspace
{
double f(double)
}
Im Namensbereich ist jetzt eine Funktion f() deklariert, deren Namen nicht mit einer
globalen Funktion f() kollidiert, denn sie heißt vollständig beispielspace::f().
So muß sie bei ihrer Definition auch genannt werden:
double beispielspace::f(double x)
{
return sqrt(x);
}
Using-Deklaration und -Direktive
Aus Namensbereichen kann man Bezeichner durch Voranstellen des ScopeOperators verwenden, man importiert bspw. einzelne Namen aus dem
Namensbereich mit using beispielspace::f oder man inportiert den ganzen
Namensbereich mit using namespace beispielspace;.
59
Die Programmiersprache C++
Der erste Fall ist praktisch, wenn man eine Funktion innerhalb eines lokalen
Gültigkeitsbereichs öfters aufrufen muß und sich die Schreibarbeit des qualifizierten
Aufrufs mit Namen und Scope-Operator sparen möchte. Außerhalb von Funktionen
ist die Verwendung unsicher, denn es könnte Namenskollisionen geben. Allerdings
werden sie zum Zeitpunkt der Übersetzung festgestellt, sind also nicht
problematisch.
Die zweite Version, das Importieren eines kompletten Namensbereichs, ist prizipiell
eine Übergangslösung, weil Namensbereiche noch nicht von allen Compilern
unterstützt werden.
Man braucht den Namen einer Funktion nicht zu qualifizieren, wenn sie innerhalb
einer anderen Funktion aufgerufen wird, dessen Paramter aus demselben
Namensbereich stammen, z.B.:
namespace Zeit
{
class datum { /* ... */ };
string format(const Datum&);
// ....
};
}
void f(Zeit::Datum d)
{
string s = format(d);
// ...
} // f
Auch wenn im Kontext von f() keine Funktion format() vorhanden ist, so wird sie
dennoch gefunden, denn im Namensbereich des Paramters gibt es eine passende
Funktion.
Aliasnamen für Namensbereiche
Bei häufigem Verwenden eines Namensbereichs ist es praktischer, wenn der Name
kurz ist. Andererseits ist das Risiko von Namenskollisionen bei sehr kurzen Namen
hoch. Beim Entwurf eines Namensbereichs sind lange, beschreibende Namen
zweckmäßig. Der Anwender kann leicht einen kurzen Namen erzeugen, z.B.:
namespace kurz = langer_und_eindeutiger_Name;
Jetzt kann man mit kurz::meineFunktion() leichter qualifizieren.
Unbenannte Namenbereiche
Oft möchte man Namenskollisionen verhindern, aber nicht ständig neue, garantiert
eindeutige Namen erfinden. Namen von Namensbereichen können schließlich immer
noch kollidieren. Unbenannte Namensbereiche können hier eine Hilfestellung sein.
namespace
{
void f()
{
// ..
}
}
60
Die Programmiersprache C++
Die Funktion f() ist im Namensbereich versteckt. Bei einen unbenannten
Namensbereich ist ein implizites using namespace vorhanden. Da Namen des
Namensbereichs nur in der aktuellen Übersetzungseinheit (also in der vorliegenden
Quelldatei) bekannt sind, sind Namenskollisionen mit anderen Quelldateien
ausgeschlossen.
1.5 Ausdrücke
Ein Ausdruck ist eine Folge von Operatoren, Operanden und Interpunktionszeichen,
die eine Berechnung definiert. Die einfachste Form eines Ausdrucks ist eine
Konstante oder Variable, z.B.
3.14159
"Fachhochschule Regensburg"
obereGrenze
Die Auswertung von Ausdrücken richtet sich nach bestimmten Konvertierungs- und
Gruppierungsregeln, nach der Abarbeitungsreihenfolge und der Richtung der
Operatoren, dem Vorhandensein von Klammern und den Datentypen der
Operanden.
Ausdrücke werden allgemein durch Konstante, Bezeichner, indizierte bzw. nicht
indizierte Variable, Verweise auf Strukturen, Funktionsaufrufe gebildet, die sowohl
mit unären als auch binären Operatoren verknüpft sind. Die Reihenfolge bei der
Auswertung der Ausdrücke ist bestimmt durch Vorrang und Assoziativität der
Operatoren. Die meisten C++-Operationen sind linksassoziativ. Man unterscheidet
arithmetische, logische Operatoren, Zuweisungsoperatoren, Operatoren zur
Speicherverwaltung und eine Reihe sonstiger Operatoren. Einige der Operatoren
können sowohl unäre als auch binäre Operationen bestimmen.
1.5.1 Arithmetische Operatoren
Neben den vier Grundrechnungsarten Addition (+), Subtraktion (-), Multiplikation (*)
und Division (/) steht eine fünfte (sog. Modulo-Operation) zur Verfügung- Sie
berechnet den Rest einer Division zweier ganzer Zahlen.
Operator
*
/
%
+
-
Funktion
multipliziere
dividiere
Modulo-Operator für ganze Zahlen
addiere
subtrahiere
Anwendung
ausdruck1 * ausdurck2
ausdruck1 / ausdruck2
ausdruck1 % ausdruck2
ausdruck1 + ausdruck2
ausdruck1 - ausdruck2
Nur das negative Vorzeichen "-" ist zugelassen. Es hat den Vorrang vor den
Operatoren *, / und %, diese wiederum haben Vorrang vor +, -46. Läßt sich mit dieser
46
Punktrechnung geht vor Strichrechnung, die modulo-Operation zählt zur Punktrechnung)
61
Die Programmiersprache C++
Regel keine eindeutige Auswertungsreihenfolge festlegen, dann wird von links nach
rechts ausgewertet. Nach der Zuweisung
A = 10 * 2 * 3 + 5 * 2;
enthält die Variable A den Wert 70. Überschaubarer wäre folgende Form
A = (10 * 2 * 3) + (5 * 2);
Klammern verändern die Auswertungsreihenfolge, z.B.:
A = 10 * 2 * (3 + 5) * 2;
führt für A zum Wert 320.
1.5.2 Relationale und logische Operatoren
In C++ ist jeder Ausdruck wahr, der nicht Null ist.
if (xa = 20) cout << "\nSinnlose Bedingung";
ist für C++ immer wahr, da sich am Wert der Zuweisung nichts ändern wird. In der
Regel wird aber im Innern der Klammer eine echte Bedingung stehen, z.B.
if (z == '+')
Das doppelte Gleichheitszeichen hat ausgewertet den Wert 1, wenn die Bedingung
erfüllt ist, andernfalls 0. Die Kombination von Bedingungen übernehmen logische
Operatoren.
Operator
!
>
>=
<
<=
==
!=
&&
||
Funktion
logisches NICHT
größer als
größer gleich
kleiner
kleiner gleich
gleich
ungleich
logisches UND
logisches ODER
Anwendung
!ausdruck
ausdruck1 > ausdruck2
ausdruck1 >= ausdruck2
ausdruck1 < ausdruck2
ausdruck1 <= ausdruck2
ausdruck1 == ausdruck2
ausdruck1 != ausdruck2
ausdruck1 && ausdruck2
ausdruck1 || ausdruck2
Der logische NOT-Operator ! ergibt wahr, falls sein Operand den Wert 0 hat. Im
anderen Fall bewertet er mit falsch.
Die relationalen Operatoren ergeben den Wert 0, falls der Vergleich bzw. das
Ergebnis falsch ist, und 1, falls es wahr ist.
&& ergibt den Wert 1, falls die beiden Operatoren von 0 verschieden sind,
andernfalls 0. Der rechte Operand wird nur ausgewertet, wenn der linke Operand
nicht 0 ist Der logische Operator || gibt den Wert 1 zurück, falls jeder der Operatoren
verschieden von 0 ist, andernfalls 0. Der 2. Operand wird nicht ausgewertet, falls der
1. verschieden von 0 ist. Anschaulich lassen sich die Ergebnisse logischer
Operatoren durch sog. Wahrheitstafeln darstellen:
62
Die Programmiersprache C++
&&
Bedingung 1
0
Bedingung 2
1
0
0
0
1
0
1
!
Bedingung
0
1
1
0
Bedingung 2
0
1
||
0
0
1
1
1
1
Bedingung 1
Abb. 1.5-1: Wahrheitstafeln zur Beschreibung logischer Operatoren
Aufgabe: Überprüfe, welches Wort das folgende Programm 47 ausgibt.
#include <iostream.h>
int main()
{
int a = 2, b = 5, c = 7;
if (!(a < b && c <= a + b || a - b < c))
cout << "\nROT";
else cout << "\nGRUEN";
}
1.5.3 Bitweise logische Operatoren
Sie können nur auf ganzzahlige Werte verwendet werden.
Operator
~
<<
>>
&
|
^
Funktion
Bit-Komplement
Shiften nach links
Shiften nach rechts
bitweises UND
bitweises inklusives ODER
bitweises exklusives ODER
Anwendung
~ausdruck
ausdruck1 << ausdruck2
ausdruck1 >> ausdruck2
ausdruck1 & ausdruck2
ausdruck1 | ausdruck2
ausdruck1 ^ ausdruck2
Shift-Operatoren verschieben Bit-Muster um eine angegebene Anzahl von Bits nach
links bzw. nach rechts. In Abhängigkeit vom Vorzeichen fallen Nullen oder Einsen in
die freien Positionen. Der &-Operator wird gewöhnlich eingesetzt, um bestimmte Bits
auf 0 zu setzen, der |-Operator um bestimmte Bits auf 1 zu setzen.
Die beiden Variablen
short xa = 5;
short xb = 11;
werden durch folgende Anweisung verknüpft:
cout << "xa & xb = " << xa & xb;
47
PR15201.CPP
63
Die Programmiersprache C++
Die Ausgabe ist nach Ausführung der Anweisung: xa & xb = 1
Das Ergebnis kann man mit Hilfe der internen Zahlendarstellung des Rechners
nachvollziehen:
xa = 0000000000000101
& xb = 0000000000001011
----------------------0000000000000001
Die „ODER-Verknüpfung“ ergibt
xa = 0000000000000101
| xb = 0000000000001011
----------------------0000000000001111
Beim „exklusiven ODER“ erhält man eine 1, falls die Operanden verschieden sind:
xa = 0000000000000101
^ xb = 0000000000001011
----------------------0000000000001110
Der „Bit-Komplement-Operator“ kippt alle Bits seines Arguments. So liefert
cout << "~xa = " << ~xa;
die Ausgabe ~xa = -6
xa = 0000000000000101
~xa = 1111111111111010
Die Darstellung ist das sog. Zweierkomplement48. Das vorderste Bit bestimmt das
Vorzeichen. Eine 1 bedeutet negative Zahl, eine 0 positive Zahl. Die Zahl, die nur
Einsen enthält, steht im Zweierkomplement für den Wert -1. Addiert man hierzu eine
1, erhält man den Wert 0. Zieht man eine 1 ab, dann wird die letzte Stelle zu 0, alle
übrigen bleiben 1. So kommt man schließlich auf „-6“.
Beim „Schieben nach Links (<<)“ werden an den freiwerdenden Stellen grundsätzlich
Nullen nachgeschoben. Beim Shift nach rechts werden bei vorzeichenlosen Zahlen
immer Nullen („logical shift“, bei vorzeichenbehafteten Zahlen das Vorzeichenbit 49
nachgeschoben („arithm. shift“), z.B.
xa << 2;
xa >> 2;
// = 0000000000010100
// = 0000000000000001
Ein Shift um eine Position nach links entspricht einer Multiplikation mit 2, und ein
Shift nach rechts verhält sich wie eine ganzzahlige Division durch 2.
Generell kann man einen Shift um n Positionen nach links bzw. nach rechts einer
Multiplikation bzw. einer ganzzahligen Division mit 2 n gleichsetzen
Bsp.: Bestimmen der int-Länge mit Bitoperatoren
#include <iostream.h>
sog. „unechtes Komplement“; das Zweierkomplement wird gebildet, indem alle Bits invertiert werden und
dann auf das Ergebnis 1 addiert wird
49 Das Bit an der am weitesten links stehenden Position
48
64
Die Programmiersprache C++
int main()
{
unsigned n, w = ~0;
for (n = 0; (w & 01) != 0; w = w >> 1, n++)
// for (n = 0; w & 01; w >>= 1, n++)
;
cout << "\nDie Wortlaenge von int betraegt "
<< n << " Bits";
}
1.5.4 Zuweisungsoperatoren
Eine Zuweisung hat die Form: L-Wert = R-Wert
„L-Wert (lvalue)“ ist das Ziel der Zuweisung. Er ist ein Ausdruck, der ein Objekt
bezeichnet, das in der Lage ist seinen Wert zu ändern. „R-Wert“ wird an der Stelle
abgespeichert, die durch „L-Wert“ bezeichnet ist.
Mit dem Zuweisungsoperator = wird einer Variablen ein Wert zugewiesen (bzw. eine
Variable erhält durch den Zuweisungsoperator einen Wert), z.B.:
x = 20;
.....
x = x + 1;
Nach der rechten Seite des Gleichheitszeichens steht die Variable x als R-Wert. Sie
wird geladen. Nach dem Laden wird sie um 1 vermindert. Der neue Wert wird an die
Stelle geschrieben, die die Variable auf der linken Seite angibt (L-Wert).
Anstelle von
x = x - 1;
kann man auch schreiben50
x -= 1;
Die abkürzende Schreibweise ist auch für andere Operatoren zugelassen, z.B.:
fx *= 100;
laenge += 1;
derg /= 100.0;
// fx = fx * 100
// laenge = laenge + 1;
// derg = derg / 100.0;
Operatoren dieser Form sind so definiert, daß der Ausdruck x op = y mit dem
Ausdruck x = x op y gleichbedeutend ist. So ist bspw. „a *= b + c“
gleichwertig zu „a = a * (b + c) “, da die Addition eine höhere Priorität besitzt
als der Operator „*= “.
Das Operationszeichen (+, -, *, /, usw.) steht immer vor dem Operator der
Zuweisung. Die Anweisung
x -= 1;
ist grundsätzlich verschieden zu
50
Hinweis: Diese Fassung läuft um Bruchteile von Sekunden schneller ab
65
Die Programmiersprache C++
x =-1;
// ist dasselbe x = -1;
1.5.5 Inkrement- und Dekrementoperatoren
Die Operatoren -- und ++ sind eine weitere Spezialität von C++. So ist
n = (x++) * (y++) gleich n = x * y
und
n = (++x) * (++y) gleich n = (x + 1) * (y + 1)
Stehen die Operatoren -- bzw. ++ vor der Variablen, dann spricht man von
Prädekrement bzw. Präinkrement. Stehen sie dahinter, dann spricht man von
Postdekrement bzw. Postinkrement. Interessant werden diese Operatoren
insbesondere dann, wenn sie mit anderen Ausdrücken kombiniert werden, z.B.:
b = a-- entspricht
b = a; a = a - 1;
b = --a;
entspricht
a = a - 1; b = a;
Durch die abkürzende Schreibweise können aber auch Probleme auftreten, z.B.:
a
b
c
c
= 10;
= 20;
= 30;
+= --b + a++ - 5;
// c = 30 + 19 + 10 - 5 = 54
1.5.6 Bedingungsoperator
Er besitzt die Form: ausdruck ? ausdruck1:ausdruck2
Falls „ausdruck“ einen Wert verschieden von 0 besitzt, ist das Ergebnis der Wert von
„ausdruck1“ andernfalls von „ausdruck2“.
Bsp.:
#include <iostream.h>
int main()
{
int i = 10, j = 20, k = 30;
cout << "Der groessere Wert von " <<
<< (i > j ? i : j) << endl;
66
i << " und " << j << " ist "
Die Programmiersprache C++
cout << "Der Wert von " << i << " ist " << (i % 2 ? " " : " nicht ")
<< "ungerade" << endl;
// Auch verschachtelte "arithmetische if" sind moeglich, z.B. wenn
// die Variable "groesster" auf den groessten Wert der drei Variablen
// gesetzt werden soll
int groesster = ( ( i > j) ? ((i > k) ? i : k) : (j > k) ? j : k);
cout << "Der groesste Wert von " << i << " , " << j
<< " und " << k << " ist " << groesster << endl;
}
1.5.7 Kommaoperator
Mehrere Ausdrücke lassen sich über den Kommaoperator sequentiell auswerten.
Der Ausdruck „ausdruck1, ausdruck2, ... , ausdruckn “ wird von links nach rechts
ausgewertet. Der Wert des gesamten Ausdrucks entspricht dem Wert von
„ausdruckn“.
Bsp.:
#include <iostream.h>
main()
{
int a = 0, b = 0, c;
c = (a++, b++, b++, b++);
cout << "a = " << a << "b = " << b << "c = " << c << endl;
}
Der Kommaoperator wird gelegentlich in den Bestandteilen einer for-Schleife
benutzt, z.B. „Berechnung der Summe der Zahlen 1 bis 100“
for (int i = 1, sum = 0; i <= 100; sum += i, ++i)
67
Die Programmiersprache C++
1.5.8 Vorrang und Assozitivität von Operatoren
Die folgende Zusammenstellung beginnt mit Operatoren hoher Priorität und endet
bei Operatoren mit niedrigerer Priorität.
R
L
L
L
L
L
R
R
R
R
R
R
R
R
L
L
L
L
L
L
L
L
L
L
L
L
L
L
Operator
::
::
->
[]
()
()
sizeof
++, -~
!
+, *, &
()
new, delete
->*, .*
*, /. %
+, <<, >>
<, <=, >, >=
==, !=
&
^
|
&&
||
?:
=, *=, /=, %=, +=
-=, <<=, >>=, |=,
^=
,
Bedeutung
global scope
class scope
member->auswahl
Vektoren
Funktionsaufruf
Wertkonstruktion
Objekt-, Typgröße
De-, Inkrementierung
Komplement
Negation
unäres Minus, Plus
Dereferenz, Referenz
Typumwandlung
Kreieren, Löschen
Komponentenzeigerdereferenzierung
multiplikative Operation
Arthm. Operationen
Links-, Rechtsshift
relationale Operatoren
gleich, ungleich
bitweises UND
bitweises exkl. ODER
bitweises inkl. ODER
logisches UND
logisches ODER
arithmetisches if
Zuweisungsoperator
Beispiel
::name
::klassenName :: member
pointer->member
pointer[ausdruck]
ausdruck(ausdruckliste)
type(ausdruckliste)
sizeof(type)
lvalue++; ++lvalue
~ausdruck
!ausdruck
-ausdruck; +ausdruck
*ausdruck; &lvalue
(type) ausdruck
new type; delete p
ausdruck1 * ausdruck2
ausdruck1 + ausdruck2
lvalue << ausdruck
ausdruck1 < ausdruck2
ausdruck1 == ausdruck2
ausdruck1 & ausdruck2
ausdruck1 ^ ausdruck2
ausdruck1 | ausdruck2
ausdruck1 && ausdruck2
ausdruck1 || ausdruck2
Kommaoperator
Vielen Operatoren für benutzerdefinierte Datentypen kann durch Überladen eine
beliebige Bedeutung51 zugeordnet werden. Die syntaktischen Eigenschaften der
Operatoren (Assoziativität, Präzedenz) können allerdings nicht verändert werden.
51
z.B. die spezielle Bedeutung der Operatoren << und >> im Zusammenhang mit Ein- und Ausgabe
68
Die Programmiersprache C++
1.6 Anweisungen
1.6.1 Einfache Anweisung
Anweisungen steuern die Ablaufkontrolle während der Ausführung eines
Programms. Enthält das Programm keine Sprung- oder Auswahlanweisungen, dann
werden Anweisungen nacheinander (sequentiell) ausgeführt.
Die einfache Anweisung oder Ausdrucksanweisung52 wird durch ein Semikolon ;
abgeschlossen. Durch Auswerten des Ausdrucks wird die Anweisung ausgeführt. Die
häufigste Form der einfachen Anweisung ist die Zuweisung (Opertor: =). Die leere
Anweisung besteht nur aus einem abschließenden Semikolon und hat keine
Wirkung. Eine Anweisung kann auch aus mehreren anderen Anweisungen
zusammengesetzt sein, der dazu gebildete Block wird durch geschweifte Klammern
({}) eingeschlossen.
1.6.2 Kontrollanweisungen (-strukturen)
Alle Kontrollstrukturen testen logische Werte auf 0 bzw. ungleich 0. C++ interpretiert
0 als den logischen Wert falsch, einen von 0 verschiedenen Wert als wahr.
1. Die "for"-Anweisung
for (ausdruck1; ausdruck2; ausdruck3) anweisung;
ausdruck1 wird einmal bewertet und bestimmt die Anfangsanweisung (Initialisierung
des Schleifenzählers). Demnach wird, solange ausdruck2 ungleich Null ist, der
Anweisungsteil gefolgt von ausdruck3 (Manipultion des Schleifenzählers) ausgeführt.
Bsp.:
void bsort(int* x, int n)
{
// Sortieren durch Austauschen
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (x[i] > x[j])
tauschen(x,i,j);
}
Falls der 1. und 2. Ausdruck fehlen, so bedeutet dies, daß an entsprechender Stelle
nichts ausgeführt werden soll. Ein fehlender 2. Ausdruck wird jedoch stets als wahr
interpretiert (Endlos-Schleife), z.B.:
#include <iostream.h>
main()
{
52
Jedem Ausdruck, dem ein Semikolon folgt, ist eine Ausdrucksanweisung
69
Die Programmiersprache C++
// Vor Ablauf des Programs Unterbrechungsmoeglichkeiten feststellen
for (int i = 1;/* kein 2. Ausdruck */;i++)
cout << i << endl;
}
Jeder Ausdruck der "for"-Schleife ist optional und kann wegfallen. Jeder Ausdruck
hat aber eine bestimmte Funktion, so daß mindestens das Semikolon bleiben muß:
for(;;)
;
// leere Anweisung
tut eigentlich nichts, das aber unendlich oft.
Beim Verändern der Schleifenvariablen im ausdruck3 einer for()-Schleife spielt es
keine Rolle, ob der Operator ++ bzw. -- vor oder hinter der Schleifenvariablen steht.
Die Anweisung wird immer nach dem Durchlaufen des Schleifenrumpfs ausgeführt.
ausdruck 1
ausdruck 2
Rumpf
ausdruck 3
Abbruchbedingung
Abb. 1.61: Schematischer Programmablauf in einer for()-Schleife
Man kann auch mehr als 3 Anweisungen im Schleifenkopf realisieren. So ist bspw.
beim Sortieren u.U. nötig, mehr als 2 Laufvariablen zu kontrollieren. Eine zählt von 1
aufwärts, die andere von einer Obergrenze abwärts. Treffen sich beide Variablen,
dann soll die Schleife abbrechen:
for (int i = 0, int j = Obergrenze; i <=j; i++, j--)
ausdruck1 und ausdruck3 sind hier zusammengesetzt bzw. durch ein Komma
getrennt. Sie werden syntaktisch wie eine Anweisung behandelt.
Bsp.53:
#include <iostream.h>
void main()
{
int i, j;
for (i = 0, j = 9; ((i <= 9) && (0 <= j)); i++, j--)
cout << "\ni = " << i << " j = " << j;
}
53
PR16202.CPP
70
Die Programmiersprache C++
2. Die "while"-Anweisung54
while ( ausdruck ) anweisung;
Der Anweisungsteil wird ausgeführt solange ausdruck ungleich Null ist bzw. bis
ausdruck den Wert Null (falsch) ergibt..
Bsp.: „Sortieren durch Einfügen“
void einfuegesortieren(int *x, int n)
{
int schl, j;
for (int i = 1; i < n; i++)
{
schl = x[i];
j
= i - 1;
while ((schl < x[j]) && (j >= 0))
{
x[j + 1] = x[j];
j--;
}
x[j + 1] = schl;
}
}
3. Die "do"-Anweisung55
do anweisung while ( ausdruck );
Sie wird immer dann angewendet, falls die Schleife mindestens einmal durchlaufen
werden soll. Eine do-Anweisung wird solange ausgeführt. bis ausdruck den Wert Null
(falsch) annimmt.
Bsp.:
1) Fibonacci-Zahlen56
Alle Glieder einer (Fibonacci-) Folge, deren Werte eine eingegebene Größe
(„maxwert“) nicht überschreitet, sollen berechnet werden. In der Fibonacci-Folge
1, 1, 2, 3, 5, 8, 13, ...
kann ab dem 3. Glied jedes weitere Glied aus der Summe der beiden
vorhergehenden Glieder berechnet werden:
#include <iostream.h>
void main()
{
int maxwert, c;
int a = 1, b = 1;
cout << "\nMaxwert? ";
cin >> maxwert;
cout << '*' << endl;
do
54
kopfgesteuerte Schleife
fußgesteuerte Schleife
56 PR16206.CPP
55
71
Die Programmiersprache C++
{
for (int i = 1; i <= b; i++)
cout << '*';
cout << endl;
c = a + b;
a = b;
b = c;
} while (c < maxwert);
}
2) Sortieren durch Zerlegen „Quicksort"
Der Algorithmus besteht aus folgenden Arbeitsschritten:
(1) Auswahl eines Elements „test“ aus der zu sortierenden Liste
(2) Zerlegen des zu sortierenden Bereichs in 2 Teilbereiche
- Teilbereich 1 enthält alle Elemente, die kleiner sind als „test“
- Teilbereich 2 enthält alle Elemente, die größer sind als „test“
(3) Sortien der Teibereiche (rekursiv)
void quicksort(int *x, int links, int rechts)
{
int i = links;
int j = rechts;
// Auswahl eines Elements aus dem zu sortierenden Teilbereich
int test = x[(links + rechts) / 2];
do {
// Zerlegen in 2 Teilbereiche
while (x[i] < test) i++;
while (test < x[j]) j--;
if (i <= j)
{
tauschen(x,i,j);
i++;
j--;
}
} while (i <= j);
if (links < j) quicksort(x,links,j);
if (i < rechts) quicksort(x,i,rechts);
}
4. continue und break
continue
bewirkt, daß der Rumpf einer Schleife nicht bis zum Ende ausgeführt wird, sondern
sofort ein neuer Schleifendurchgang startet.
break
Verlassen der Schleife, d.h. Sprung hinter das Schleifenende.
Alle 3 Schleifenarten lassen sich mit Hilfe von break vorzeitig verlassen. Tritt der
Befehl im Schleifenrumpf auf, wird an das Ende gesprungen und die Schleife nicht
wiederholt.
Bsp.:
#include <iostream.h>
int main()
{
int zaehler;
cout << "Start einer Schleife mit continue " << endl;
72
Die Programmiersprache C++
for (zaehler = 1; zaehler <= 10; zaehler++)
{
if (zaehler > 5) continue;
cout << zaehler << endl;
}
cout << "Nach der for-Schleife, zaehler = " << zaehler << endl;
cout << "Start einer for-Schleife mit break " << endl;
for (zaehler = 1; zaehler <= 10; zaehler++)
{
if (zaehler > 5) break;
cout << zaehler << endl;
}
cout << "Nach der for-Schleife, zaehler = " << zaehler << endl;
}
5. "if"-Anweisung
if (x[i] > x[j]) tauschen(x,i,j);
ist eine bedingte Anweisung. Der Aufruf der Funktionsprozedur „tauschen“ erfolgt nur
dann, falls die vorstehende Bedingung erfüllt ist.
if (ausdruck) anweisung1; else anweisung2;
Der "else" Anteil ist optional.
Fallunterscheidungen hängen generell davon ab, ob die arithmetische Auswertung
des ausdruck von 0 verschieden ist. „ausdruck“ muß einen "integer"-Wert liefern.
Jeder Wert ungleich Null bewirkt die Durchführung von "anweisung1", das Ergebnis
Null führt zur Aktivierung von "anweisung2".
Bsp.: "Ausgabe der Zeichenfolge 12345789 "
#include <iostream.h>
int main()
{
int i;
for (i = 1; i <= 10; i++)
{
if (i == 6)
continue;
else cout << i;
}
}
Mehrere Anweisungen nach einer Bedingung müssen
zusammengefaßt werden. So gibt bspw. die Bedingung
if (x == 100)
{
cout << "Die Variable x ";
cout << "enthaelt den Wert 100 \n";
}
nur aus, falls x tatsächlich den Wert 100 hat. Wird angegeben
if (x == 100)
cout << "Die Variable x ";
cout << "enthaelt den Wert 100\n";
73
zu
einem
Block
Die Programmiersprache C++
führt das zur Ausgabe (auch wenn x nicht den Wert 100 hat): enthaelt den Wert
100
Hinter der schließenden, runden Klammer einer if-Konstruktion steht in der Regel
kein Semikolon. Allerdings wird
if (i == 0);
cout << "Untergrenze erreicht\n";
keine Warnung oder Fehlermeldung des Compiler hervorrufen. Das einzelne
Semikolon wird als leere Anweisung betrachtet. Es wird hier deshalb Untergrenze
erreicht ausgegeben.
Bsp.:
#include <iostream.h>
main()
{
int i;
for (i = 1; i <= 10; i++)
{
if (i == 6)
;
else cout << i;
}
}
Im „else-Teil“ sind weitere bedingte Anweisungen möglich.
6. Die "switch"-Anweisung
switch ( ausdruck )
{
case konstante1: /* Folge von Anweisungen */
break;
case konstante2:
.....
case konstanten: /* Folge von Anweisungen */
break;
default: /* Folge von Anweisungen */
}
/* Weitere Anweisungen */
Diese Anweisung bewirkt in Abhängigkeit vom Wert eines Ausdrucks eine mehrfach
verzweigte Auswahl. Der Ausdruck wird bewertet und auf Gleichheit mit einer Anzahl
von Konstanten verglichen. Die "default"-Marke ist optional und wird angesprungen,
wenn keine der Konstanten zutrifft. Nach "case" steht eine Folge keiner oder
mehrerer Anweisungen.
Die „break“-Anweisung am Ende von „case“ ist optional. Sie sorgt dafür, daß der
Programmablauf zum Ende des „switch“-Statement verzweigt.
Die „switch“-Anweisung bewertet den Ausdruck und prüft auf eine Übereinstimmung
mit einer Konstanten. Wird die Übereinstimmung gefunden, wird die Kontrollstruktur
switch ab dieser Konstanten (Marke) bis zur schließenden geschweiften Klammer
oder bis zum nächsten „break“ abgearbeitet, z.B.
#include <iostream.h>
74
Die Programmiersprache C++
main()
{
char z;
cin >> z;
while (z != '#')
{
switch(z)
{
case '+':
case '-':
case '*':
case '/':
case '%':
cout << "arihmetische Operator " << endl;
break;
case '&':
case '^':
case '!':
cout << "Bit-Verknuepfung" << endl;
break;
default:
cout << "Sonstiges" << endl;
break;
}
cin >> z;
}
cout << "Schleife abgebrochen" << endl;
}
7. „goto“-Anweisung „goto marke“
Die „goto“-Anweisung verzweigt die Programmsteuerung zu der Anweisung mit dem
Sprungziel marke.
marke: anweisung
marke ist ein Bezeichner. Der Gültigkeitsbereich erstreckt sich auf die Funktion, d.h.
das Sprungziel muß sich innerhalb derselben Funktion befinden.
75
Die Programmiersprache C++
goto
Anweisung;
goto marke:
Anweisung;
marke:
Anweisung;
if (Ausdruck)
wahr
Anweisung
Anweisung
1
if - else
falsch
2
switch
switch (Ausdruck)
case 1
case 2
default
Folge
Folge
Folge
Folge
Ausdruck
solange Ausdruck
2
Folge
for
1
wahr ist
Anweisung;
Ausdruck
3
while
solange Ausdruck wahr ist
Anweisung;
do - while
Anweisung;
solange Ausdruck wahr ist
Abb. 1.6-2: Kontrollstrukturen in C++ als Nassi-Shneidermann-Diagramme
76
Die Programmiersprache C++
1.7 Funktionen
1.7.1 Definition und Deklaration von Funktionen
Eine Aufgabe wird in C++ normalerweise über den Aufruf einer Funktion gelöst, die
die Aufgabe bearbeitet. In der Definition ist spezifiziert, wie die Funktion arbeitet.
Eine Funktion kann erst aufgerufen werden, nachdem sie deklariert ist.
Deklaration von Funtionen
Sie enthält den Namen der Funktion, den Typ des Funktionswerts, Anzahl der Typen
der Argumente, die der Funktion übergeben werden:
ergebnistypname funktionsname(parameterliste);
Defintion von Funktionen
ergebnistypname funktionsname (typname1 parametername1, typname2
parametername2, ..... , typnameN parameternameN)
{
vereinbarungen
anweisungen
}
ergebnistypname ist der Resultattyp der Funktion. Ohne Typangabe wird „integer" angenommen.
funktionsname ist der Name der Funktion
parameternamen sind eine Liste von Argumenten (formale Parameter), die durch Kommas getrennt
sind. Die Parameter 1..N sind innerhalb des Funktionsrumpfs gültig. Die Liste der Parameter kann
auch leer sein, auch die Anweisungsfolge im Funktionsrumpf darf ausgelassen werden, z.B.:
dummy(){}.
vereinbarungen sind Definitionen von lokalen Variablen und Deklarationen von Objekten, deren
Gültigkeitsbereich auf die Funktion beschränkt sein soll.
anweisungen sind Aktionen, die ausgeführt werden, falls die Funktion aufgerufen wird.
Eine Funktions-Definition ist eine Funktionsdeklaration, die (zusätzlich) den
Prozedurrumpf der Funktion enthält.
Bsp.:
void tauschen(int* x, int i, int j)
{
int zwischen = x[i];
x[i] = x[j];
x[j] = zwischen;
}
77
Die Programmiersprache C++
1.7.2 Parameterübergabe (Übergabe der Argumente)
Beim Aufruf einer Funktion wird für jeden formalen Parameter Speicherplatz
reserviert und mit den Werten der aktuellen Parameter belegt. Der Typ des formalen
Parameters wird mit dem Typ des aktuellen Arguments abgeglichen, und es werden
alle standardmäßigen und benutzerdefinierten Typumwandlungen durchgeführt.
funktionsname(parameterliste_der_aktuellen_Parameter);
Bsp.:
#include <iostream.h>
void f(int wert, int& ref)
{
wert++; ref++;
}
main()
{
int i = 1; int j = 1;
f(i,j); // Aufruf der Funktion mit den aktuellen Parametern
cout << "i = " << i << " j = " << j << endl;
}
Das erste Argument von f() wird "by value" übergeben, das 2. Argument "by
reference" .
1. Call by value
Die Werte aktueller Parameter ändern sich bei Abarbeitung der Funktion nicht. Im
folgenden Programm wechselt die Funktion tausch() die lokalen Kopien ihrer
Argumente.
Bsp.57:
#include <iostream.h>
void austausch(int v1, int v2)
{
int hilf = v2;
v2 = v1;
v1 = hilf;
}
main()
{
int i = 10;
int j = 20;
cout << "vor dem austausch() \t i: " << i << "\t j: " << j << endl;
austausch(i,j);
cout << "nach dem austausch() \t i: " << i << "\t j: " << j << endl;
return 0;
}
57
PR17201.CPP
78
Die Programmiersprache C++
Die aktuellen Variablen, die "tausch()" zum Auswechseln übergeben bekommt,
bleiben davon unberührt. Diese Problem des call by value kann man auf zwei
alternativen Lösungswegen umgehen:
1. "tausch()" kann über die Deklaration der formalen Parameter zu Zeigern umgeschrieben werden:
void zaustausch(int* v1, int* v2)
{
int hilf = *v2;
*v2
= *v1;
*v1
= hilf;
}
In main() erfogt der Aufruf von tausch() dann so: zaustausch(&i,&j);
2. Die formalen Argumente werden vom Typ "reference" vereinbart
void raustausch(int &v1, int &v2)
{
int hilf = v2;
v2
= v1;
v1
= hilf;
}
In main() erfogt der Aufruf dann so: raustausch(i,j);
Falls Zeiger getauscht werden sollen, wäre folgende Behandlung von tausch() möglich:
#include <iostream.h>
void zraustausch(int *&v1, int *&v2)
{
int *hilf = v2;
v2 = v1;
v1 = hilf;
}
main()
{
int i = 10;
int j = 20;
int *zi = &i; int *zj = &j;
cout << "vor dem austausch() \t i: " << *zi << "\t j: " << *zj << endl;
zraustausch(zi,zj);
cout << "nach dem austausch() \t i: " << *zi << "\t j: " << *zj << endl;
return 0;
}
2. Call by reference
Bei der Übergabe einer Referenz auf ein Objekt, kann die Funktion die Werte der
aktuellen Parameter ändern. Außerdem kann ein formaler Parameter der Funktion
als Referenz definiert werden. Der Name des formalen Paramters ist dann ein
anderer Name für das Objekt, das durch den aktuellen Parameter bestimmt wird.
Die Übergabe "by reference" kann speziell für große Objekte sehr effizient sein.
Falls Argumente dann nicht geändert werden sollen, sollte das Argument als const
deklariert sein. Damit wird angezeigt: Die Übergabe "by reference" geschieht aus
Effizienzgründen, die Funktion kann den Wert des Arguments nicht verändern.
79
Die Programmiersprache C++
3. Vektoren als Parameter
Wird ein Vektor (C-Array) als Parameter einer Funktion verwendet, so wird nur die
Adresse auf den Anfang des Vektors übergeben. Ein Argument vom Typ T[] wird in
den Typ T* konvertiert, falls er Parameter eines Funktionsaufrufs ist. Die Funktion
arbeitet auf den aktuellen Parameter und kann ihn verändern.
Vektoren (C-Arrays) werden in C++ immer mit "call by reference" übergeben.
4. Default-Argumente (Standardwerte für Funktionsargumente)
Sie dürfen am Ende der Parameterliste auftreten. Default-Argumente werden bei der
Deklaration der Funktion typüberprüft und beim Aufruf der Funktion ausgewertet.
Bsp.:
1) 58
#include <iostream.h>
void striche(int, int, char z = '-');
int main()
{
striche(0,40);
striche(0,40,'$');
striche(0,40,'@');
striche(0,40,'+');
char zeichen;
cin >> zeichen;
return 0;
}
void striche(int spalte, int anz, char z)
{
for (int i = spalte; i < anz; i++)
cout << z;
cout << endl;
}
2) 59
#include <iostream.h>
int addiere(int i, int j, int m = 0, int n = 0)
{
return (i + j + m + n);
}
void main()
{
int a = 1, b = 5, c = 10, d = 20;
cout << addiere(a,b) << '\n';
cout << addiere(a,b,c) << '\n';
cout << addiere(a,b,c,d) << '\n';
}
58
59
PR17202.CPP
PR17204.CPP
80
Die Programmiersprache C++
Für Parameter dürfen Standardwerte (default values) angegeben werden. Sie
werden vom Übersetzer eingesetzt, falls das entsprechende Argument im Aufruf
fehlt.
5. Beliebig viele Argumente (variable Parameterlisten)
Für Funktionen, bei denen Anzahl und Typ der Argumente nicht exakt feststeht,
endet die Argumentenliste mit einer Ellipse (...). Damit wird ausgedrückt: Es können
weitere Argumente folgen. Ein gut entworfenes Programm wird höchstens einige
wenige solcher Funktionen benötigen. Überladene Funktionen und Funktionen mit
Default-Argumenten ermöglichen in den meisten Fällen ordnungsgemäße
Typüberprüfungen. Nur wenn Anzahl und Typ der Argumente unbekannt sind, ist die
Ellipse notwendig (wichtigster Anwendungsbereich: Schnittstellen-Spezifikation zu
C-Bibliotheken die zu einer Zeit entworfen wurden, als andere Alternativen nicht
verfügbar waren).
Bsp.: Die C-Ausgabe-Funktion int printf(const char* format,...) erzeugt
für eine beliebige Anzahl von Argumenten formatierte Ausgaben, deren Gestalt vom
Format-String "format" kontrolliert wird. Ein vollständiger Aufruf von printf() ist
nach dem folgenden Schema aufgebaut:
printf(format, wert1, wert2, ....)
Der Format-String besteht aus zwei Typen von Elementen:
-
einfache Textzeichen (die in den Ausgabestrom kopiert werden)
Umwandlungs-Angaben (eingeleitet durch das Zeichen %)
Die Textzeichen werden unmittelbar ausgegeben, für Umwandlungsangaben
(Formatelemente) wird der Text ausgegeben, der durch die Umwandlung eines
Wertarguments entsteht, z.B.:
#include <stdio.h>
main()
{
char* vorname = "juergen";
char* nachname = "sauer";
printf("Hallo Informatiker\n");
printf("Mein Name ist %s %s\n", vorname, nachname);
// %s erwartet char*
printf("%d + %d = %d\n",2,3,5);
// %d erwartet int-Argument
}
Das 1. Argument von printf() ist ein Format-String mit speziellen Zeichenfolgen,
die es printf() erlauben, die übrigen Elemente richtig zu verarbeiten (z.B. %s,
%d). Der Compiler weiß das nicht und kann somit u.U. nicht sicherstellen, daß die
erwarteten Argumente tatsächlich vorhanden sind. Das „%“-Zeichenmit dem
folgenden Konvertierungszeichen (code) dient zur Formatierung, so daß printf()
Zahlenvariable auch als Text ausgegeben kann. Einige der am häufigsten
verwendeten Formatelemente sind:
d: dezimal
x: hexadezimal
o: oktal
81
Die Programmiersprache C++
u:
c:
s:
e:
f:
g:
dezimal (ohne Vorzeichen)
einzelnes Zeichen
Zeichenkette
dezimale Gleitkommadarstellung (scientific)
dezimale Festkommadarstellung (fixed)
kürzeste Version von e und f
Mit den folgenden Zusätzen kann über printf() eine Ausrichtung bzw. eine
Anpassung erreicht werden:
-: linsbündige Ausrichtung
+: rechtsbündige Ausrichtung
l: langer Ausdruck
Formatelemente bestehen aus dem %-Zeichen, einer optionalen Angaben zur
minimale Feldbreite mit evtl. (durch Punkt getrennt) einer Angabe zur Anzahl der
Stellen nach dem Dezimalpunkt und dem „Code“. Der Code definiert den Typ des
betroffenen Werts und die Art der Umwandlung.
Bsp.: Aufbau einer ASCII-Tabelle60
// #include <iostream.h>
#include <stdio.h>
void main()
{
/*
int x = 32;
// 0 .. 31 sind Steuercodes und nicht darstellbar
while (x <= 256)
{
printf("\nZeichen: %c Dezimal: %d Hexadezimal: %x",
x, x, x);
x = x + 1;
}
/* fuehrt zur gleichen Ausgabe:
for (int x = 32; x <= 256; x++)
cout << "\nZeichen: "
<< (char) x
<< " Dezimal: "
<< dec << x
<< " Hexadezimal " << hex << x;
*/
}
Kann eine Funktion ermitteln, wieviel aktuelle Parameter von welchem Datentyp bei
einem bestimmten Aufruf tatsächlich angegeben werden, dann sind variable
Parameterlisten möglich. Zu Beginn einer derartigen Parameterliste steht
zweckmäßigerweise dann die Ausgabe des aktuellen Parameters (z.B. anzArg).
Dann gilt bspw. für das bestimmen des kleinsten Parameterwerts für eine Reihe
ganzzahliger Paramertypen: int min(int anzArg, int a, int b, ...)
Die 3 Punkte ... am Ende der Liste der formalen Parameter bezeichnen das
„Auslassungszeichen (Ellipse)“ und geben an, daß mehr aktuelle als deklarierte
formale Parameter übergeben werden dürfen 61. Der Zugriff auf die durch ...
vereinbarten formalen Parameter kann über sie durch stdarg.h vereinbarten
Makros va_list, va_start, va_arg, va_end erreicht werden, z.B.62:
60
PR17205.CPP
An Stelle der 3 Punkte sind beliebige Parameter erlaubt, ohne Rücksicht darauf, welche Art Argument die
Funktion tatsächlich erwartet. Durch das Auslassungszeichen wird der Typkontrollmechanismus von C++ außer
Kraft gesetzt.
62 PR17203.CPP
61
82
Die Programmiersprache C++
#include <iostream.h>
#include <stdarg.h>
int min(int anzArg, int a, int b, ...)
{
// Hilfsvariable für variable Parameter
va_list argumente;
va_start(argumente,b);
// "b" letzter Parameter der Funktion, mit dessen
// Hilfe der Makro die Anfangsadresse der variablen Para// meterliste ermittelt
int m = a;
if (b < m) m = b;
for (int i = 3; i <= anzArg; i++)
{
int par = va_arg(argumente, int);
// sukzessive Anwendung des Makro va_arg
// Der Datentyp der Argumente ist int
if (par < m) m = par;
}
va_end(argumente); // Schliessen der Bearbeitung "Parameterliste"
return m;
}
int main()
{
int a = 20, b = 30, c = 40, d = 50;
cout << min(4,a,b,c,d) << endl;
}
6. Prototypen
In C++ muß jede Funktion einen Funktionsprototyp63 haben. Ein Prototyp gibt an,
welchen Rückgabewert die Funktion hat, wie sie heißt und welche Parametertypen
sie bekommt. Abgeschlossen wird diese Angabe durch ein Semikolon. Der
Funktionsprototyp muß immer vor der ersten Stelle auftreten, an der die Funktion
genutzt wird. Sinnvollerweise sammelt man sie zu Beginn des Programms.
Grundsätzlich ist es gleich, ob eine Funktion vor oder hinter main() steht. Die
einzige Bedingung ist, daß sie vor dem ersten Aufruf durch ein Prototyp bekannt
gemacht wurde. Es hat sich eingebürgert, die Funktion main() entweder als erste
oder letzte Funktion in einem Programm einzusetzen.
63
d.h. vor ihrem Aufruf deklariert bzw. definiert sein
83
Die Programmiersprache C++
1.7.3 Funktionswerte (Ergebniswertrückgabe)
Funktionen, die nicht mit void deklariert sind, müssen einen Funktionswert
zurückgeben. Der Funktionswert wird durch eine return-Anweisung festgelegt:
return return_ausdruck
Der Wert des return_ausdrucks entspricht dem Rückgabewert der Funktion und muß
dem vereinbarten Typ entsprechen, z.B.:
#include <iostream.h>
int fak(int n)
{ return (n > 1) ? n * fak(n-1) : 1; }
int main()
{
int n;
cout << "Berechne die Fakultaet von ";
cin >> n;
cout << endl << "Ergebnis: " << fak(n) << endl;
}
Der Typ des "return"-Ausdrucks wird mit dem Typ des Funktionswerts abgeglichen
und gegebenenfalls alle standardmäßigen und benutzerdefinierten Umwandlungen
durchgeführt. Trifft das Programm auf eine return-Anweisung, dann wird die
Ausführung des Funktionsaufrufs beendet. Ist kein return vorhanden, dann endet
die Ausführung an der letzten schließenden Klammer des Funktionsausrumpfes.
Falls der Rückgabewert vom Typ void ist, dann kann die return-Anweisung auch
weggelassen werden bzw. nur „return“ angegeben werden.
Auch Referenzen können als Funktionswert zurückgegeben werden. Da Referenzen
L-Values sind (d.h.: Sie können verändert werden), kann ein Funktionsaufruf auf der
linken Seite einer Zuweisung stehen oder als Referenzparameter an eine andere
Funktion übergeben werden.
Bsp.: Eine Minimum-Funktion bestimmt die kleinere von zwei Variablen. Dieser
Variablen soll der Wert 0 zugewiesen werdenbzw. ein Wert von der Tastatur
eingelesen werden.
int &min(int &a, int &b)
{
if (a < b) return a;
return b;
}
int x, y;
// Zuweisen von 0
min(x,y) = 0;
// Einlesen Wert von der Tastatur
cin >> min(x,y);
84
Die Programmiersprache C++
1.7.4 Rekursion
1.7.4.1 Rekursive Funktionen
Eine Funktion ist rekursiv, falls die Ausführung des Rumpfs der Funktion wiederum
zum Aufruf der Funktion führt. Man unterscheidet
direkte Rekursion (Eine Funktion ruft sich selbst im Rumpf wieder auf)
und
indirekte Rekursion (Der rekursive Aufruf befindet sich nicht im Funktionsrumpf. So ruft bspw. eine
Funktion A eine Funktion B auf und diese startet in ihrem Rumpf die Funktion A).
Wie Wiederholungsanweisungen neigen auch rekursive Prozeduren zur Gefahr
nicht abbrechbarer Berechnungen. Eine Terminierung ist unbedingt erforderlich. Das
geschieht über eine Bedingung, von der der rekursive Aufruf abhängt. Falls x bspw.
die Menge der Programmvariablen ist, dann muß es eine Funktion f(x) geben, die
die Abbruchbedingung ( wie in den while-Schleifen) festlegt. Nachzuweisen ist , daß
f(x) bei jeder Ausführung der Rekursion abnimmt. Besonders einfach ist der
Nachweis der Terminierung dann, wenn der rekursiven Funktionsprozedur der
ganzzahlige Parameter n bzw. N zugeordnet wurde. Der rekursive Aufruf mit dem
Parameterwert n - 1 bzw. N - 1 garantiert das strikte Abnehmen des Wertes von
f(x) .
Bei jeder rekursiven Anwendung wird ein Satz lokaler, gebundener Variablen kreiert.
Sie haben zwar diesselben Namen wie die Objekte des vorangegangenen Aufrufs
der Prozedur, besitzen aber verschiedene Werte. Die Namen beziehen sich immer
auf die zuletzt erzeugten Variablen.
Bsp. für einfache rekursive Funktionen
C++ ermöglicht die Definition rekursiver Funktionen.
N
1. Berechnung von N !   n
n 1
Man kann dies aber auch so schreiben: N!  N  ( N  1)! für N  1 und 0!  1 .
Das führt unmittelbar zur Lösung:
long fak(int n)
{
if (n == 0) return 1;
else return n * fak(n-1);
}
2. Zurückführung der Multiplikation auf die Addition
long mult(int a, int b)
{
if (b == 1) return(a);
else return(mult(a,b - 1) + a);
}
3. Potenzieren
85
Die Programmiersprache C++
In C++ ist kein spezieller Operator dafür vorgesehen. Dieser Operator kann aber
leicht über eine rekursive Funktionsprozedur simuliert werden:
long potenz(int x, int n)
{
if (x == 0) return(0);
else if (n == 0) return(1);
else return(potenz(x,n-1)*x);
}
bzw.
double potenz(float x, int n)
{
if (x == 0.0) return(0.0);
else if (n == 0) return(1);
else if (n < 0) return(potenz(x,n+1)/x);
else return(potenz(x,n-1)*x);
}
Das Beispiel zeigt, daß man auch mit gebrochene Zahlen Potenzen bilden kann, z.B.
05
. 2  0.25 . Ebenso sind Hochzahlen mit negativen Werten (Ganzzahlen) möglich,
1
1
 4.
z.B.: 0.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)64 auftreten:
#include <iostream.h>
void striche(short st)
{
for (int i = 1; i <= st; i++)
cout << "- ";
}
unsigned long fib(short xN, short stufe)
{
unsigned long x, y;
cout << "\n";
striche(stufe);
64
Rekursionstiefe: Anzahl der Aufrufe der Funktion seit Beginn der Programmausführung minus der Anzahl der
Rückgaben an das ausführende Programm
86
Die Programmiersprache C++
cout << " Eingang N = " << xN;
if (xN <= 1)
{
cout << "\n";
striche(stufe);
cout << " Ausgang: N = " << xN << " X = " << x << " Y = " << y;
return(xN);
}
else
{
x = fib(xN-1,++stufe);
--stufe;
y = fib(xN-2,++stufe);
--stufe;
cout << "\n";
striche(stufe);
cout << " Ausgang: N = " << xN << " X = " << x << " Y = " << y;
return(x + y);
}
}
void main()
{
short n;
unsigned long ergebnis;
cout << "\nBerechnung von Fibonacci-Zahlen\n";
cout << "Argument: ";
cin >> n;
ergebnis = fib(n,0);
cout << '\n';
cout << "\nFib(" << n << ")=" << ergebnis;
}
Das vorliegende Programm führt bei einem Aufruf bspw. zu dem folgenden Protokoll:
Eingang: N = 5
- Eingang: N = 4
- - Eingang: N =
- - - Eingang: N
- - - - Eingang:
- - - - Ausgang:
- - - - Eingang:
- - - - Ausgang:
- - - Ausgang: N
- - - Eingang: N
- - - Ausgang: N
- - Ausgang: N =
- - Eingang: N =
- - - Eingang: N
- - - Ausgang: N
- - - Eingang: N
- - - Ausgang: N
- - Ausgang: N =
- Ausgang: N = 4
- Eingang: N = 3
- - Eingang: N =
- - - Eingang: N
- - - Ausgang: N
- - - Eingang: N
- - - Ausgang: N
- - Ausgang: N =
- - Eingang: N =
- - Ausgang: N =
- Ausgang: N = 3
Ausgang: N = 5 X
Ergebnis: 5
3
=
N
N
N
N
=
=
=
3
2
=
=
=
=
2
X
2
=
=
=
=
2
1
1
X
=
2
=
=
=
=
2
1
1
X
1
1
0
0
X
=
1
1 X = 11721 Y = 1
0
0 X = 11721 Y = 1
X = 1 Y = 0
X = 1 Y = 0
= 1 Y = 1
X = 11721 Y = 1
X = 11721 Y = 1
= 1 Y = 0
2 Y = 1
1
1 X = 11721 Y = 1
0
0 X = 11721 Y = 1
X = 1 Y = 0
X = 1 Y = 0
= 1 Y = 1
3 Y = 2
87
Die Programmiersprache C++
Jeder Aufruf mit N > 1 führt zu zwei weiteren Aufrufen. Das zeigt die folgende
grafische Darstellung der Aufrufhierarchie:
Aufruf: Fib(5)
4
3
3
2
1
2
1
1
2
0
1
1
0
0
Abb. 1.8-1: Schematische Darstellung der Aufrufhierarchie von Fib(5)
Insgesamt finden für Fib(5) 15 Aufrufe statt, für den Aufruf Fib(6) sind es bereits 31.
Viele Aufrufe und damit Berechnungen werden wiederholt, obwohl sie bereits an
anderer Stelle berechnet wurden. Allgemein wird die Funktion Fib() bei Eingabe einer
beliebigen positiven, ganzen Zahl N genau 2 N-1 - 1 mal aufgerufen. Man sagt: Der
Algorithmus verhält sich proportional zu 2N-1 65bzw. 2N.
Allgemein ist ein Algorithmus von exponentieller Komplexität, falls es eine
beliebige Basis M66 gibt, zu der er sich bei N Eingabewerten wie MN verhält. Für die
Größenordnungen, in denen solche Komplexitätsbetrachtungen stattfinden, spielt es
keine Rolle, welchen konkreten Wert M besitzt. Man spricht nur noch von
exponentieller Komplexität. Der Aufwand eines Algorithmus steigt also
exponentiell mit seinen Eingabewerten.
Im vorliegenden Fall bedeutet dies: Falls die Eingabewerte für Fibonacci-Zahlen zu
groß gewählt wurden, kann eine rekursive Berechnung dieser Zahlen nicht mehr
erfolgen. Das Problem liegt darin, daß ein Aufruf von Fib() zwei weitere nach sich
zieht. Allerdings könnte man hier auf die Rekursion zweckmäßigerweise verzichten.
Es läßt sich ein iterativer Algorithmus angeben, der die Berechnung in einer Schleife
realisiert, z.B.:
unsigned long fibIt(short n)
{
unsigned long x,
// aktuelle Fibonacci-Zahl
vx = 1, // Vorgaenger fib(n-1) bzw.
vvx = 0; //
fib(n - 2)
for (short i = 1; i < n; i++)
{
x
= vx + vvx;
vvx = vx;
vx = x;
}
return(x);
}
65
66
Die 1 wird hier vernachlässigt, da sie bei großen Werten von N praktisch keinen Beitrag liefert.
hier 2
88
Die Programmiersprache C++
Ein Vergleich der rekursiven Lösung mit der iterativen Lösung zeigt: Die rekursive
Lösung ist eigentlich unbrauchbar. Auf rekursive Lösungen sollte man verzichten,
wenn es iterative Lösungsmöglichkeiten gibt.
5. Gegeben ist auf rekursiver Basis die mathematische Defintion einer Funktion
1


Fn ( x)  
x
((2 n  1)  x  F ( x)  ( n  1)  F ( x)) / n
n 1
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);
}
}
89
Die Programmiersprache C++
1.7.4.2 Rekursion und Iteration
Die in den letzten Beispielen gezeigten Umwandlungen einer Rekursion in eine
Iteration ist kein Zufall. Man kann zeigen, daß sich jeder rekursive Algorithmus in
einen iterativen umwandeln läßt. Da die Iteration in den meisten Fällen wesentlich
effizienter ist als die Rekursion, ist die Frage berechtigt, weshalb man überhaupt die
Rekursion verwendet.
Für die Rekursion spricht:
1. Es gibt bestimmte rekursiv formulierte Algorithmen, die schneller oder wenigstens gleich schnell
arbeiten als vergleichbare iterative.
2. Es lassen sich viele Probleme rekursiv „sehr einfach“ lösen.
Rekursiv formulierte Algorithmen bieten sich insbesondere an, wenn das
zugrundeliegende Problem oder die zu behandelnden Daten rekursiv definiert sind.
Das ist aber noch keine Garantie dafür, daß ein rekursiver Algorithmus auch der
beste Weg zur Lösung des Problems ist.
Bsp.67: Addition einer unbekannten Zahl von ganzzahligen Werten
#include <iostream.h>
int s = 0;
void addierezahlen()
{
int x;
if ((cin >> x) > 0)
{
// Terminierung: Eingabe eines Buchstaben bzw. Sonderzeichens
s += x;
addierezahlen();
}
}
int main()
{
addierezahlen();
cout << "Die Summe ist: " << s << endl;
}
Das vorliegende Programm ist ein Beispiel für eine „tail recursion68“. Es gibt nur
einen rekursiven Aufruf, der am Ende der Funktion addierezahlen() erfolgt. Er
unterscheidet sich nicht wesentlich von einem Sprung an den Anfang der Funktion.
Daher kann addierezahlen() einfach durch die folgende iterative Version ersetzt
werden:
// Iterative Version
void addierezahlen()
{
int x;
while ((cin >> x) > 0) s += x;
}
67
68
PR17307.CPP
Endrekursion
90
Die Programmiersprache C++
Hinsichtlich der
vorzuziehen, da
Programmeffizienz
ist
die
iterative,
nichtrekursive
Version
- beim Einlesen einer große Zahlenmenge die rekursive Version einen Stapelüberlauf bewirken kann
- auch die Rückgabeadresse69 gespeichert werden muß
Nur im Stapel ist der einzige sichere Speicherplatz, da hier die letzten auf den Stapel
geschobenen Daten zuerst wieder entnommen werden. Neben der
Rückgabeadresse werden auch lokale Variable (z.B die Variable x) auf den Stapel
gebracht. Die Stapelgröße ist natürlich begrenzt. Ein häufiges Aufrufen derselben
rekursiven Funktion führt daher zum Überlaufen. Zu diesem Stapelüberlauf kommt
es nur dann, wenn die Rekursion einen bestimmten Wert übersteigt.
Unmittelbar nach dem Programmstart hat die main()-Funktion die Rekursionstiefe
1, und alle anderen Funktionen die Rekursionstiefe 0. Der zum Ablaufzeitpunkt
benötigte Speicherplatz ist dann:
S  d 1  m1  d 2  m 2 ...d n  m n
d: Rekursionstiefe
m: Menge an Speicherplatz
1..n: Indizes zur Bestimmung der vorliegenden Funktionen
Generell sollte man daher auf die Verwendung von Rekursionen immer dann
verzichten, falls es eine offensichtliche Lösung mit Iteration gibt. Solche Lösungen
existieren bspw. nicht, wenn die rekursive Funktion mehr als einen rekursiven Aufruf
enthält.
1.7.4.3 Türme von Hanoi
1. Aufgabenstellung und allgemeine Lösung
Aufgabenstellung: Es soll ein Turm mit „n“ Scheiben, die der Größe nach geordnet
liegen, scheibenweise von Quellplatz (K) nach Zielplatz (G) transportiert werden. Ein
Ausweichplatz (S) steht zur Verfügung. Es darf aber nur eine kleinere auf eine
größere Scheibe gelegt werden.
Allgemeine Lösung der Ausgabe: Zum Transport von „n“ Scheiben sind Sn Schritte
notwendig. Es gilt folgende rekursive Beziehung (mit S 1 = 1):
S n  2  (S n 1 )  1
Sn  2 n  1
Die allgemeine Lösung der Aufgabe besteht in
69
d.h. die Stelle, an der nach der Ausführung der gerufenen Funktionsprozedur die Programmkontrolle
übergeben wird
91
Die Programmiersprache C++
1) Transport von (n - 1) Scheiben nach dem Hilfsplatz
Quellplatz
Hilfsplatz
2) Verlagerung von Scheibe „n“ nach Zielplatz
Quellplatz
Zielplatz
3) Transport von (n - 1) Scheiben nach Zielplatz
Zielplatz
Hilfsplatz
Abb. 1.8-2: Beschreibung der Problemlösung
In der vorliegenden Abbildung werden zunächst 4 der 5 Scheiben vom Quellplatz auf
einen Hilfsplatz bewegt. Danach wird die größte Scheibe vom Quellpaltz auf den den
Zielplatz gebracht. Anschließend bewegt man die 4 Scheiben vom Hilfsplatz auf den
Zielplatz und zwar so70, wie man zuvor die 5 Scheiben von Quellplatz auf Zielplatz
bewegt hat.
Wie der Algorithmus das im Detail bewältigt, zeigt die folgende Darstellung, die
vollständig alle Schritte bei der Verlagerung von 3 Scheiben wiedergibt:
70
das ist der eigentliche Trick, den die Rekursion ermöglicht
92
Die Programmiersprache C++
Quellplatz
Zielplatz
Hilfsplatz
Abb. 1.8-3: Beschreibung der Lösung für drei Scheiben
In jedem Schritt wird ein um eine Scheibe kleinerer Stapel bewegt. Die Rekursion
bricht offensichtlich ab, falls keine Scheibe mehr bewegt werden muß.
93
Die Programmiersprache C++
2. Objektorientierte Darstellung
Ein objektorientiertes Programm kann man sich als Abbildung von Objekten der
realen Welt in Software vorstellen. Die Abbildungen selbst werden Objekte genannt.
Klassen sind die Beschreibungen von Objekten.
Eine Klasse ist ein „Abstrakter Datentyp“ (ADT), d.h. die Abstraktion von ähnlichen
Eigenschaften und Verhaltensweisen ähnlicher Objekte.
Ein Objekt hat einen inneren Zustand, der durch andere Objekte oder Elemente der
in der Programmiersprache vorgegebenen Datentypen dargestellt wird.
Der Zustand kann sich durch Aktivitäten (Operationen), die auf Objektdaten
durchgeführt werden, ändern. Die von jedem benutzbaren Operationen bilden die
„öffentliche Schnittstelle“, gekennzeichnet durch das Schlüsselwort „public“.
Ein Objekt ist die konkrete Ausprägung einer Klasse, es belegt im Gegensatz zur
Klasse Bereiche im Speicher, die Werte von Objekteigenschaften darstellen.
Eine in C++ formulierte Klasse hat eine typische Gestalt:
class Klassenname
{
private71:
typ attribut1;
typ attribut2;
// ...
public:
typ elementfunktion1();
typ elementfunktion2();
// ...
};
In einem Klassendiagramm wird das so beschrieben:
Klassenname
private:
typ attribut1;
typ attribut2;
// ...
public:
typ elementfunktion1();
typ elementfunktion2()
// ...
Abb.: Das Klassendiagramm der UML
Der Gültigkeitsbereich von Klassenelementen ist lokal zu der Klasse. Die Daten sind
„private“, d.h.: Sie sind von außen nicht sichtbar. Alles nach dem Schlüsselwort
„public“ ist öffentlich zugänglich.
71
das Schlüsselwort private kann entfallen, weil die Voreinstellung private ist
94
Die Programmiersprache C++
3. Implementierung in objektorientierter Darstellung72
/*
Die Tuerme von Hanoi
------------------In einer Tempelstadt sollen sich 3 Podeste aus Kupfer, Gold und
Siber befunden haben. der Sage nach soll das Ende der Welt gekommen sein, falls es jemand gelingt:
- 100 Scheiben, die auf dem Kunpferpodest liegen, abzutragen und in
derselben Reihenfolge auf dem Golpodest aufzuschichten.
- Scheiben duerfen auch auf dem Silberpodest abgelegt werden
- Es ist nicht erlaubt, dass eine kleinere Scheibe auf einer
groesseren abgelegt wird
*/
#include <iostream.h>
// Beschreibung der Klasse hanoi
class hanoi
{
private:
char quellplatz,
hilfsplatz,
zielplatz;
short n;
public:
void bewegeTurm(short,char,char,char);
};
// Beschreibung der in der Klasse deklarierten Schnittstellen
// (Methoden)
void hanoi :: bewegeTurm(short n, char quellplatz,
char zielplatz,
char hilfsplatz)
{
if(n > 0)
{
bewegeTurm(n-1, quellplatz, hilfsplatz, zielplatz);
cout << "\n\t\tBewege Scheibe von Turm " << quellplatz
<< " nach Turm " << zielplatz;
bewegeTurm(n-1, hilfsplatz, zielplatz, quellplatz);
}
}
// Klient (client)
void main()
{
const short MAXSCHEIBEN = 16;
// Mehr dauert ewig.
short anzahl;
hanoi turm;
// Instanz, Objekt bzw. Server
/* Wie einer Variablendefinition wird für ein Objekt Speicherplatz
bereitgestellt. Zu diesem Zweck wird eine besondere Klassendefinition aufgerufen, die Konstruktor genannt wird. Der
Konstruktor wird vom System automatisch bereitgestellt, kann
aber auch selbst definiert werden.
*/
cout << "\n\n\t\t\tDie Tuerme von Hanoi";
cout << "\n\t\t\t-------------------\n\n";
72
PR17305.CPP
95
Die Programmiersprache C++
do
{
cout << "\tWieviele Scheiben sollen bewegt werden (max. "
<< MAXSCHEIBEN << "): ";
cin >> anzahl;
} while ((anzahl < 1) || (anzahl > MAXSCHEIBEN));
/* Der Server "turm" erbringt fuer den Klient "main" eine Dienstleistung */
turm.bewegeTurm(anzahl, 'K', 'S', 'G');
}
// Rekursionsanfang
cout << endl;
// Ende von main()
Auch hier ist festzustellen: Die Bearbeitung einer großen Anzahl von Scheiben
nimmt einige Rechenzeit in Anspruch. Auch hier die Komplexität dieses Algorithmus
exponentiell (2N). genau wie bei der Berechnung der Fibonacci-Zahlen zieht ein
Aufruf zwei weitere nach sich.
1.7.4.4 Damen-Problem
Das Problem wird beschrieben am einfachen, übersichtlich darstellbaren 4-DamenProblem, einer Vereinfachung des bekannten 8-Damen-Problems. Hier sollen
bekanntlich 8 Damen so auf einem Schachbrett positioniert werden, daß sie sich
nicht schlagen können, d.h.: Zwei Damen stehen nie in derselben Zeile oder Spalte
oder Diagonale. Das Verfahren kann grafisch als Baum dargestellt werden, dessen
Knoten Lösungsvektoren sind und dessen Kanten den Weg des Algorithmus von
einem Lösungsvektor der Länge (i - 1) zu einem der Länge i angeben.
(_,_,_,_)
1
2
3
2
(2,_,_,_)
(1,_,_,_)
4
1
S
S
(1,3,_,_)
2
S
(1,4,_,_)
4
2
S
3
3
4
4
(2,4,_,_)
1
S
(2,4,1,_)
(1,4,2,_)
(2,4,1,3)
Abb. 1.8-4: Plan zur Lösung des 4-Damen-Problems
Von der Ausgangssituation (keine Dame ist positioniert, Wurzel) werden bestimmte
Lösungsvariablen verfolgt. Im ungünstigsten Fall führt die bearbeitete Variante in
96
Die Programmiersprache C++
eine Sackgasse. In diesem Fall muß der Variantenpfad so weit zurückverfolgt
werden, bis man zu einem Knoten mit einem noch nicht bearbeiteten Nachfolger
kommt. Man spricht bei dieser Vorgehensweise auch von Tiefensuche in
vorliegenden Baumgraphen und nennt die Verfahrensweise Backtracking.
Die Lösung läßt sich anschaulich darstellen:
0
-3
3
y
x
x
x
x
x
2
8
5
Abb. 1.8-5: „Eindimensionale Beschreibung“ des 4-Damen-Problems
Da Damen sich auch in der Diagonale bedrohen, sind auch Haupt- und Nebendiagonalplätze zu überwachen.
Es sollen zur Lösung des 8-Damen-Problems acht Zahlen ermittelt werden. Die
Zahlen stehen von links nach rechts für die Position der Dame in der entsprechenden Spalte.
/*
Das Problem der acht Damen
-------------------------Kurzbeschreibung: Durch Backtracking werden alle Moeglichkeiten
gefunden, acht Damen so auf einem Schachbrett
zu positionieren, dass keine Dame eine andere
bedroht.
*/
#include <iostream.h>
class damen
{
private:
97
Die Programmiersprache C++
unsigned short pos[8];
bool Zeile[8];
bool HD[15];
// Hauptdiagonale "/"
bool ND[15];
// Nebendiagonale "\"
public:
damen(); // Konstruktor
void versuche(int);
friend ostream& operator <<(ostream &, damen &); // Ausgabe
};
// ------ Schnittstellendefinition (Methoden) ------------------damen :: damen()
{
int i;
// Im Konstruktor werden die einzelnen Felder initialisiert.
for(i = 0; i < 8; i++) Zeile[i] = true;
for(i = 0; i < 15; i++) HD[i] = ND[i] = true;
}
void damen :: versuche(int j)
{
for(int i = 0; i < 8; i++)
{
if(Zeile[i] && HD[j+i] && ND[7+i-j])
{
// Position ist moeglich, also wird Dame gesetzt:
pos[i] = j;
// Dadurch werden alle oben ueberpr•ften Zeilen und
// Diagonalen bedroht:
Zeile[i] = HD[j+i] = ND[7+i-j] = false;
/* Falls noch nicht alle Damen gesetzt wurden,
erfolgt ein weiterer, rekursiver Aufruf, der die
naechste Spalte ueberprueft.
*/
if(j < 8) versuche(j+1);
else // Sonst wurde moegliche Loesung gefunden
cout << (*this);
/* Nun muss der Versuch noch zurueckgenommen werden,
da er am nach Verlassen dieser Spalte wieder
moeglich wird
*/
Zeile[i] = HD[j+i] = ND[7+i-j] = true;
} // Ende von if( ...
}
// Ende von for( ...
}
// Ende von Versuche()
// --------- non-member-Funktionen --------------------ostream& operator<<(ostream &s, damen &d)
{
s << "\n\t";
for(short i = 0; i < 8; i++)
s << d.pos[i] << "
";
return(s);
}
// --------Klient: main-program -----------------------int main()
{
damen queens;
// Instanz, Server
queens.versuche(1);
// Rekursionsanfang
}
// Ende von main()
98
Die Programmiersprache C++
Es gibt insgesamt 92 Lösungen73, wobei jedoch nur 12 tatsächlich verschieden sind.
die übrigen entstehen durch Permutation der Zeilen, d.h.: Die Dame in der 1. Spalte
wird in diesselbe Zeile der 2. Spalte, die Dame in der zweiten in dieselbe Zeile der
der 3. Spalte gesetzt, bis schließlich die Dame in der 8 Spalte in diesselbe Zeile der
1. Spalte gesetzt wird.
1.7.5 Überladen von Funktionsnamen (overloading)
Funktionen mit gleichen Funktionsnamen aber unterschiedlichen Parameterdatentypen
Diese Technik wird in C++ für elementare Operationen bereits genutzt. Es gibt nur
einen Namen für die Addition (, nämlich +,) der für die Addition von Integer-,
Gleitpunkt- und Pointer-Typen verwendet werden kann. Normalerweise muß der
Compiler entscheiden, wenn eine Funktion mit dem Namen f aufgerufen wird,
welche Funktion mit dem Namen f gemeint ist. Dies geschieht über einen Vergleich
der aktuellen Argumenttypen mit den deklarierten Argumenttypen. Die am besten
passende Funktion wird aufgerufen. Gibt es keine "am besten passende Funktion"
wird ein Compiler-Fehler erzeugt.
Bsp.
#include <iostream.h>
int
addiere(int a, int b)
{ return a + b; }
float addiere(float a, float b){ return a + b; }
int main()
{
int a = 3; int b = 2;
float x = 3.14159; float y = 5.6782;
cout << "a + b = " << addiere(a,b) << endl;
cout << "x + y = " << addiere(x,y) << endl;
cout << "a + y = " << addiere(float (a), y) << endl;
// addiere(a,y); Fehler: zweideutig
}
Das Überladen von Funktionen (Bildung von Homonymen) ist ein Mittel,
allgemeingültigere Programme zu schreiben. Es ist auch möglich, Operatoren zu
überladen. Der Shift-Operator ist bspw. ein häufig vom Anwender überladener
Operator, wenn es um die Ausgabe von Daten auf dem Bildschirm geht.
Ein Aufruf überladener Funktionen ist in zwei Fällen möglich:
1. Die Typen der aktuellen Parameter stimmen genau mit denen der formalen Parameterliste einer
überladenen Funktion überein.
2. Es existieren Typumwandlungen, mit denen Typen der aktuellen Parameter mit den Typen einer
formalen Parameterliste zur Deckung gebracht werden können.
Technisch funktioniert das dadurch, daß an den eigentlichen Funktionsnamen die
Liste der Parameter codiert angefügt wird (sog. „name mangling“). Der
Rückgabetyp wird nicht in den Namen codiert. Genausowenig wird zwischen einem
73
vgl. PR17308.CPP
99
Die Programmiersprache C++
Argument als „value“-Parameter und einem Argument gleichen Typs als ReferenzParameter unterschieden. Die große Familie der in Deklarationen möglichen
Spezifizierer und Modifizierer erschweren Compiler und Programmierer die
Entscheidung, ob Funktionen mit denselben Namen unterschiedliche
Parameterlisten besitzen oder nicht. So können bspw. "reine" und ReferenzParameter nicht unterschieden werden, da ihre aktuellen Parameter identisch sind.
Grundsätzlich gilt: Sind die aktuellen Argumente zweier formaler Parameter, die sich
nur im Spezifizierer unterscheiden, identisch, dann ist Überladen nicht möglich, z.B.:
f(int x);
f(const int x);
f(volatile int x);
// Fehler
// Fehler
Auch
int f() bzw. int f(void)
sind in einem Gültigkeitsbereich nicht möglich.
Funktionen können nur im selben Geltungsbereich (scope) überladen werden.
Funktionen aus einem äußeren Gültigkeitsbereich werden durch die lokale
Deklaration einer Funktion gleichen Namens verborgen. Falsch ist daher
extern int f(char *);
void illegal()
{
extern double f(double); // verbirgt äußeres f
f("Leider falsch");
}
Der Compiler arbeitet mit festen Regeln bei der Ermittlung der Definition einer
Funktion, die für den jeweiligen Parameter am besten paßt.
Bsp.: Hochzählen von Zahlen und Buchstaben durch eine überladenen Funktion
#include <iostream.h>
int naechsteGroesse(int a)
{
return(++a);
}
int naechsteGroesse(int a, int b)
{
return(a + b);
}
char naechsteGroesse(char a)
{
return(++a);
}
char naechsteGroesse(char a, int b)
{
return ((char)(a + b)); // cast sorgt für korrekte Rueckgabe
}
int main()
{
cout << "\n Die naechste Groesse von 5 ist "
<< naechsteGroesse(5);
cout << "\n Die naechste Groesse von F ist "
<<naechsteGroesse('F');
100
Die Programmiersprache C++
cout <<
<<
cout <<
<<
"\n Die naechste Groesse von 5 ist nach 4 Einheiten "
naechsteGroesse(5,4);
"\n Die naechste Groesse von F ist nach 4 Einheiten "
naechsteGroesse('F',4);
}
Der Compiler bildet für jeden Parameter eine Liste von passenden Funktionsversionen. So erzeugt der Compiler für den Aufruf naechsteGroesse(‘F’,4)
folgende Listen:
Nr. des Parameters:
passende Funktion
1
naechsteGroesse(char,int)
naechsteGroesse(char)
2
naechsteGroesse(char,int)
Aus dieser Funktionsmenge bildet er dann die Schnittmenge. Ist diese leer oder
enthält sie mehr als eine Funktionsversion, so führt das zu einem Fehler, z.B.:
f1(char a, int b) { ....... }
f1(int a, char b) { ....... }
......
f1('x','y');
Default-Argumente
In C++ kann der Funktion eine variable Anzahl von Parametern übergeben werden 74,
falls Standardwerte für fehlende Parameter festgelegt werden. Liegt bspw. anstatt
char naechsteGroesse(char a, int b) folgende Darstellung der Funktion
vor char naechsteGroesse(char a, int b = 1), dann findet der Compiler
für den Aufruf naechsteGroesse('F') zwei passende Funktionen und erzeugt
einen Fehler.
Falls der Compiler Standardargumenten bei der Suche nach einer passenden
Funktionsversion berücksichtigen muß, dann ist für ihn jede Aufrufmöglichkeit eine
eigene Version. „n“ Standardargumente führen zu „n+1“ Versionen.
Konvertierung (Konversion)
Immer, wenn der Compiler keine genau passende Version findet, versucht er es mit
Konvertierungen. Allerdings kann er dann auch natürlich mehrere passende
Versionen finden. So führt ein Aufruf von naechsteGroesse('F','D') im
vorliegenden Programm zu einer internen Konvertierung75 und zu einer passenden
Version.
74
75
vgl. 1.7.2, 4.
der char-Wert ‘D’ wird in seinen entsprechenden int-Wert (68) umgewandelt.
101
Die Programmiersprache C++
Algorithmus zur Homonymenauflösung76
(1) Bestimme die Menge F jener Funktionen, die in Namen und Anzahl der
Argumente77 mit dem Aufruf übereinstimmen und deren formale
Argumentdatentypen mit den aktuellen Parameterdatentypen des Aufrufs
kompatibel (d.h. identisch oder konvertierbar) sind. Falls diese Menge höchstens
ein Element besitzt, kann der Algorithmus abgebrochen werden, ansonsten ist
mit Schritt (2) fortzufahren.
(2) Bestimme für jeden aktuellen Parameter p i die Menge Fi’ aller Funktionen aus F,
die bzgl. des Datentyps für das Argument p i am besten zum gegebenen Aufruf
passen.
Am „besten passen“ bedeutet:
- Es werden keine Konversionssequenzen mit mehr als einer benutzerdefinierten
Konversionsfunktion berücksichtigt.
- Eine kürzere Trivialkonversion wird einer längeren vorgezogen
- Die folgenden Trivialkonversionen haben im allg. keinen Einfluß auf die
Beurteilung zweier Konversionssequenzen: T -> T&, T& -> T, T[] ->
T*, T() -> (*T). T -> const T, T* -> const T*.
Es läßt sich für Argumentkonversionen eine Wertskala angeben. Konversionen,
die eine Konversion einer niederen Stufe beinhalten, sind besser als solche, die
auch Konversionen höherer Stufen miteinbeziehen:
1. Exakte Entsprechung
Es sind keine Konversionen (oder lediglich Trivialkonversionen) zur Anpassung des aktuellen
Arguments an den Typ des formalen Parameters nötig. Unter Trivialkonversion sind jene, die
„const“-Zeiger oder -Referenzen aus „nicht const-Zeigern“ oder „-Referenzen“ erzeugen,
schlechter als andere.
2. Auswertungen (Integralauswertungen und float-double-Konversionen)
Integralauswertungen sind: char, short, Aufzählungstypen, Bitfelder. Sie können immer anstelle
vom int benutzt werden. Falls Werte des ursprünglichen Datentyps durch int dargestellt
werden können, dann wird nach int umgewandelt, andernfalls nach unsigned int. Es wird
von kleinen ganzzahligen Typen in Richtung int und von kleineren gebrochenzahligen Typen
Richtung double promotet.
3. Standardkonversionen.
Der Compiler versucht. Aktuelle und formale Parameter durch Anwendung von
Standardkonversionen zur Deckung zu bringen. Alle numerischen typen oder zeichentypen
können ineinander konvertiert werden, außerdem können alle Zeiger zu void* konvertiert
werden.
4. Benutzerdefinierte Konversionen.
Es werden die benutzerdefinierten Konversionen angewandt, die in benutzerdefinierten Klassen
definiert werden.
5. Variable Parameterliste
Die Zuordung eines Arguments zum Auslassungszeichen (...) ist schlechter als jede Konversion.
(3) Bilde den Durchschnitt aller Fi’. Enthält dieses genau ein Element, nenne es f
und fahre mit Schritt (4) fort. Andernfalls ist der Aufruf illegal.
76
Vereinfachte Darstellung, im Detail beschrieben in Ellis, M.A. und Stroustrup, B.: The Annoted C++
Reference Manual, Addison Wesley, Reading MA, 1990
77 Arität
102
Die Programmiersprache C++
(4) Überprüfe durch paarweises Vergleichen, ob f für mindestens ein Argument eine
echt bessere Entsprechung darstellt als jede andere Funktion in F. Wenn dies
der Fall ist, kann f aufgerufen werden. Andernfalls ist der Aufruf illegal.
Codierung der Funktionsnamen für den Binder
Der Compiler kann mit Hilfe der angegebenen Regeln Funktionen mit gleichen
Namen anhand der Argumentdatentypen auseinander halten. Der Binder (Linker) hat
keinen Zugriff auf syntaktische Informationen wie Datentypen und kann deshalb mit
überladenen Funktionsnamen nichts anfangen. Argumentdatentypen werden
deshalb vom Compiler nach einem bestimmten implemtierungsabhängigen
Schlüssel kodiert und an den eigentlichen Namen angehängt.
So kann
int min(int anzArg, int a, int b, ...)
im Objektcode bspw. min__Fiiie heißen.
F: "globale Funktion"
i: int-Argument
e: Auslassungszeichen (ellipsis)
Die Variante double min(int anzArg, double a, double b, ...); heißt dann
min__Fidde.
Einbinden fremdsprachiger Unterprogramme
Die Namenskonvention zur Codierung von Funktionsnamen führt aber auch zu
Problemen- So ist die Kompatobilität mit C-Unterprogrammen nicht mehr gesichert.
Die C-Standardbibliotheksfunktion char* strcpy(char*, const char*) erhält
in C++ bspw. die Objektcodebezeichnung strcpy__FPcCPc .
P: Pointer
c: char
C: const
Die Funktion wird in C-Bibliotheken nur strcpy genannt und kann daher vom Linker
nicht gefunden werden.
Für diese Fälle gibt es eine spezielle Art der internen Deklaration:
extern "C" char* strcpy(char* ziel, char* quelle);
“C“ gibt an: Es handelt sich um eine C-Funktion und der Funktionsname folgt nicht
dem C++-Namensschema.
103
Die Programmiersprache C++
1.7.6 Operatorfunktionen
In Operatorfunktionen tritt an die Stelle des üblichen Funktionsnamens das
Schlüsselwort operator, gefolgt von dem zu überladenden Operator. Die
Parameterangaben sind auf die in C++ definierten Aritäten der Operatoren
beschränkt78.
In C++ können fast alle Operatoren (Ausnahmen sind: .,.*,::, ? : , sizeof) überladen
werden. Man kann deshalb die Bedeutung79 der Operatoren im Zusammenhang mit
benutzerdefinierten Datentypen (struct, class, union) frei bestimmen.
Das Überladen der Operatoren << und >> in der iostream-Klasse ermöglicht so
bspw. die Ausgabe und Eingabe von Werten zu elementaren Datentypen und
char*, void*. Man kann generell diese Technik auf benutzerdefinierte Datentypen
erweitern. Berücksichtigt man die Definition der Ausgabe- bzw. der Eingabeoperation
in der iostream-Klasse, dann führt das zur folgenden allgemeinen Beschreibung für
eine Funktion zur Ausgabe bzw. Eingabe eines beliebig definierten Datentyps:
ostream& operator<<(ostream&, const T&);
istream& operator>>(istream&, const T&);
Das 1. Argument in diesen Operatorfunktionen ist ein Ausgabe- bzw.
Eingabestrom80, das 2. Argument ist der benutzerdefinierte Datentyp. Der
Rückgabewert ist eine Referenz auf den Ausgabe- bzw. Eingabestrom.
1.7.7 Inline-Funktionen
In C++ kann eine Funktion als inline deklariert werden. Bei Aufrufen derartig
deklarierter Funktionen setzt der Compiler den zugehörigen Programmcode direkt
ein. Dadurch entfällt der beim Standard-Funktionsaufruf erforderliche Overhead der
Parameterübergabe. Der Programmcode der Funktion kann jedoch mehrfach
auftreten. Inline-Funktionen sind daher meistens nur für sehr kurze Routinen
angebracht, z.B.
#include <iostream.h>
// inline-Funktionen
inline fak(int n) { return n < 2 ? 1 : n * fak(n-1); }
inline int max(int x, int y){ return x > y ? x : y; }
inline int min(int x, int y){ return x <= y ? x : y; }
inline int abs(int i){ return (i < 0 ? -i : i); }
inline void setze(int &x, int bit){ x = (x | (1 << bit)); }
// setzt das Bit mit der Nummer bit der Ganzzahl x
inline void tausche(int& a, int& b){ int h = a; a = b; b = h; }
main()
{
int n;
cout << "Berechne die Fakultaet von "; cin
cout << "Ergebnis: " << fak(n) << endl;
78
d.h.: Defaultargumente sind nicht erlaubt
Semantik
80 das wurde in der iostream-Klasse so festgelegt
79
104
>> n;
Die Programmiersprache C++
int i, j;
cout << "Wert: "; cin >> i; cout << "Wert: "; cin >> j;
cout << "Kleinerer Wert: " << min(i,j) << endl;
cout << "Groesserer Wert: " << max(i,j) << endl;
i = abs(i); j = abs(j); cout << "Wert von i: " << i << endl;
cout << "Wert von j: " << j << endl; tausche(i,j);
cout << "Vertauschter Wert i: " << i << endl;
cout << "Vertauschter wert j: " << j << endl; setze(i,3); setze(j,5);
cout << "Bit Nr. 3 von i gesetzt " << i << endl;
cout << "Bit Nr. 5 von j gesetzt " << j << endl;
}
Wie die „register“-Angabe bei automatischen Variablen ist die „inline“-Spezifikation
lediglich ein Hinweis für den Übersetzer. der Compiler kann, muß aber nicht den
Funktionscode expandieren. Sobald die Komplexität der Funktion einen gewissen
Schwellwert überschreitet (z.B. falls Schleifen vorkommen), wird ohne Warnung eine
normale Funktion erzeugt.
1.7.8 Funktionsschablonen
C++ erlaubt allgemeine Typ-Parameterangaben für Funktionen (und Klassen). Das
ermöglicht in Funktionsparameterlisten die Angabe generischer Parameter und
Funktionsaufrufe mit Parameterangaben (zur Laufzeit), die unterschiedliche
Datentypen aufweisen. Deklarationen zu Funktionsschablonen (template functions)
beginnen mit einer Parameterliste der Form
template <class T1, class T2, .... , class Tn> ...
funktionsdefinition
Die Bezeichner Ti sind Namen, die für spezifische C++-Datentypen stehen und die
bei der Anwendung einer Funktionsschablone übergeben werden. Das Schlüsselwort „class“ steht für den Datentyp bzw. Typ. T i kann Standard-Datentyp oder ein
vom Benutzer definierter Typ sein81.
Eine Funktionsschablone definiert eine Familie überladener Funktionen, deren
Mitglieder im Bedarfsfall automatisch erzeugt werden.
Bsp.: Die Funktion
int min (int i, int j)
{
return (i < j) i ? : j;
}
kann für andere numerische Datentypen durch eine Funktionsschablone wesentlich
allgemeingültiger angegeben werden:
template <class T> T min(T a, T b)
{
return (a < b ? a : b);
}
81
Anstatt class kann auch typename geschrieben werden
105
Die Programmiersprache C++
Diese Funktion wird wie eine gewöhnliche Funktion aktiviert:
double m = min(13.11,11.13);
int m = min(13,11);
Natürlich ist diese Funktionsschablone auf den Vergleich solcher Elemente
beschränkt, die in einer eindeutige Ordnungsbeziehung zueinander stehen. Ein
derartiger unmittelbarer Vergleich ist bei Zeichenketten 82 nicht gegeben. Soll ein
solcher Vergleich zur Ermittlung der kleineren Größe stattfinden, ist eine spezielle
Funktion min() für den Typ „Zeichenkette“ anzugeben:
#include <string.h>
char* min(char* s1, char* s2)
{
return (strcmp(s1,s2) < 0 ? s1 : s2);
}
Der Compiler sucht bei der Übersetzung des vorstehenden Aufrufs zunächst eine
exakt entsprechende (normale) Funktion. Falls eine derartige Funktion nicht
vorhanden ist, werden alle verfügbaren Funktionsschablonen auf eine
möglicherweise genau (ohne jegliche Argumentenkonversion) passende Variante
untersucht. Wird eine solche Funktion gefunden, dann wird sie aus der Schablone
generiert und aufgerufen.
Für die Auswahl einer Funktion bei überladenen Funktions-Templates gelten
folgende Regeln:
1. Der Compiler sucht nach einer existenten Funktion, die in den Parametern exakt
übereinstimmt. Der sog. "exact match" ist nicht ganz trivial erklärt. Auf jeden Fall müssen die
Parameter denselben Typ haben, Dabei sind triviale Umwandlungen wie z.B. T& oder T* nach
T[] immer möglich. Existieren mehrere derartige Funktionen, wird eine Fehlermeldung wegen
Mehrdeutigkeit ausgegeben.
2. Der Compiler sucht nach einem Funktions-Template, dass die entsprechende Funktion mit
einer "exact match"-Argumentenliste erzeugen kann. Diese wird gegebenenfalls erzeugt. Bei
Mehrdeutigkeit wird ein Fehler ausgegeben.
3. Nach den üblichen Regeln wird versucht überladene Funktionen zu finden. Sofern existent
wird sie aufgerufen.
Eine *.o- oder *.obj-Datei, die vom Compiler durch Übersetzen einer Datei nur mit
Templates erzeugt wird, enthält keinen Programmcode und keine Daten. Ein
Template ist keine Funktionsdefinition, sondern eine Schablone, nach der der
Compiler erst bei Bedarf eine Funktion zu einem konkreten Datentyp erzeugt.
1.7.9 Objekte als Funktionen
Die Aufgabe einer Funktion kann von einem Objekt übernommen werden 83. Dazu
wird der Funktionsoperator () mit der Operatorfunktion operator()() überladen.
Ein Objekt kann dann wie eine Funktion aufgerufen werden. Ein algorithmisches
Objekt diesert Art wird Funktionsobjekt oder Funktor genannt.
„kleiner“ und „groesser“ ist für Zeichenketten folgendermaßen zu interpretieren: „Kleiner“ ist eine
Zeichenkette dann, wenn das 1. Element, an dem sich eine Zeichenkette von einer zweiten unterscheidet, im
ASCII-Code weiter vorn liegt
83 Die Technik wird in den Algorithmen und Klassen der C++-Standardbibliothek häufig eingesetzt.
82
106
Die Programmiersprache C++
Funktoren sind Objekte, die sich wie Funktionen verhalten, aber alle Eigenschaften
von Objekten haben.
1.7.10 Spezifikation von Funktionen
Eine Funktion erledigt eine Teilaufgabe und ändert dabei den Programmzustand. Die
Spezifikation der Zustandänderungen durch die Bedingungen, die vor und nach dem
Aufruf gelten, ist unbedingt sinnvoll. Dazu gehören
-
Annahmen über die Importschnittstelle (Eingabedaten)
Fehlerbedingungen
die Exportschnittstelle (Ausgabedaten)
Für Vor- und Nachbedingungen werden die engl. Begriffe precondition84 und
postcondition85 benutzt. Vor- und Nachbedingungen können zum Nachweis der
Korrektheit eines Programms benutzt werden.
Vor- und Nachbedingungen lassen sich über assert() verifizieren. „assert()“ ist
abgeleitet vom engl. Assertion (Zusicherung). Zusicherungen werden mit dem
Header <cassert> eingebunden.
Bsp.: Erraten eine Zahl zwischen 1 und 10086
#include <iostream.h>
#include <assert.h>
void rateSpiel(int n)
{
// Precondtion: n > 0
// Postcondition: Der Anwender wurde gebeten sich eine Zahl
//
zwischen 0 und n zu merken. Die Funktion
//
stellt eine Reihe von Fragen, bis die
//
Zahl gefunden ist
int rateAnz;
char antwort;
assert (n >= 1);
cout << "Merken Sie sich eine Zahl zwischen 1 und " << n
<< "." << endl;
antwort = 'N';
for (rateAnz = n;(rateAnz > 0) && (antwort != 'Y') && (antwort != 'y');
rateAnz--)
{
cout << "Ist die Zahl " << rateAnz << "?" << endl;
cout << "Antworte Y oder N, druecke return: ";
cin >> antwort;
}
if ((antwort == 'Y') || (antwort == 'y'))
cout << "War mir bereits bekannt";
else
cout << "Hier wurde geschwindelt" << endl;
}
int main()
{
rateSpiel(100);
84
Abkürzung pre
Abkürzung post
86 PR17999.CPP
85
107
Die Programmiersprache C++
}
108
Die Programmiersprache C++
2. Datentypen
2.1 Einfache, fundamentale Datentypen
Größe und numerische Bereiche der grundlegenden Datentypen sind implementierungsspezifisch und leiten sich von der Architektur des Rechers ab. So gilt bspw. in
Borland C++ für 16-Bit- bzw. 32-Bit-Datentypen87:
a) folgende interne Darstellung
s
15
int
long int
s
31
s: Vorzeichenbit (0 = positiv, 1 = negativ)
Abb.: 2.1-1: Ganzzahlige 16-Bit-Datentypen
short int
int,
long int
s
15
0
s
31
0
s: Vorzeichenbit (0 = positiv, 1 = negativ)
Abb.: 2.1-2: Ganzzahlige 32-Bit-Datentypen
float s Exponent
31
22
Mantisse
0
double s Exponent
63
51
Mantisse
0
s: Vorzeichenbit (0 = positiv, 1 = negativ)
Abb.: 2.1-3: Gleitkomma-Datentypen
87
Für Borland C++ ist die IBM-PC-Familie Ausgangspunkt. Somit bestimmt die Architektur der Intel 8088- und
80x86-Mikroprozessoren die Auswahl der internen Darstellung für verschiedene Datentypen
109
Die Programmiersprache C++
b) folgende Größen und Bereiche
Typ
unsigned char
Größe (Bits)
8
Bereich
0-255
char
8
-128 bis 127
enum
16
-32768 bis 32768
unsigned int
16
0 bis 65535
short int
int
16
16
-32768 bis 32767
-32768 bis 32767
unsigned long
32
0 bis 4294967295
long
32
-2147483648 bis
2147483647
float
32
double
64
34
.  10 38 bis 3.4  1038
17
.  10 308 bis 17
.  10308
long double
80
3.4  10 4932 bis 11
.  10 4932
Anwendungen, z.B.:
Kleine Zahlen,
kompl. PC-Zeichens.
Kleine Zahlen
ASCII-Zeichen
Geordnete
Wertemengen
Große Zahlen
Schleifenzähler
Zähler, kleine Zahlen
Zähler,
kleine Zahlen
Astronomische
Distanzen
sehr große
Zahlen
7-stellige Genauigkeit
15-stellige
Genauigkeit
19-stellige
Genauigkeit
Abb. 2.1-4: 16-Bit-Datentypen, Größen und Bereiche
Typ
unsigned char
char
short int
unsigned int
int
Größe (Bits)
8
8
16
32
32
unsigned long
32
enum
32
long
32
float
32
double
64
long double
80
Bereich
0 bis 255
-128 bis 127
-32768 bis 32768
0 bis 4294967295
-2147483648 bis
2147483647
0 bis 4294967295
-2147483648 bis
2147483647
-2147483648 bis
2147483647
34
.  10 38 bis 3.4  1038
17
.  10 308 bis 17
.  10308
3.4  10 4932 bis 11
.  104932
Anwendungen
Kleine Zahlen
ASCII-Zeichen
Zähler
Große Zahlen, Schleifenzähler
Zähler, kleine Zahlen
Astronomische
Genauigkeit
Geordnete Wertemengen
sehr große Zahlen
7-stellige Genauigkeit
15-stellige Genauigkeit
19-stellige Genauigkeit
Abb. 2.1-5: 32-Bit-Datentypen, Größen und Bereiche
Die in einem C++-System zutreffenden Zahlenbereiche findet man in der Datei
limits88. C++ bietet die Möglichkeit, den Zahlenbereich mit einer Funktion
abzufragen, z.B.:
#include <iostream>
#include "c:\cppbuch\include\limits"
88
Im Header <limits> wird die Template-Klasse numeric_limits definiert. Sie hat Spezialisierungen für
die ganzzahligen Grunddatentypen bool, char, signed char, unsigned char, short, unsigned
short, int, unsigned int, long, unsigned long sowie für die Gleitkommazahltypen float,
double und long_double.
110
Die Programmiersprache C++
using namespace std;
int main()
{
cout << "Der Zahlenbereich fuer int geht von"
<< numeric_limits<int>::min()
<< " bis "
<< numeric_limits<int>::max()
<< endl;
}
Ganze Zahlen (int) verschiedener Größe werden beschrieben durch:
char
short int
int
long int
// dient zur Darstellung einzelner Zeichen
// int kann entfallen
// int kann entfallen
Die Typen char, short, int und long sind standardmäßig vorzeichenbehaftet, d.h.:
Sie können postive und negative Zahlen darstellen. In C++ könnte man auch dafür
signed int, signed short und signed long schreiben.
Zur Definition positiver Bezeichner kann unsigned 89herangezogen werden, z.B.:
unsigned int x;
Insgesamt kann man 9 verschiedene "Integer"-Typen durch Kombination von "short",
"long", "unsigned" mit "int" unterscheiden.
+
++
-+
*
/
%
=
*=
/=
%=
+=
-=
<
>
<=
>=
==
!=
<<
>>
&
^
89
Unäres Plus
Unäres Minus
Vorherige Inkrementierung um Eins
Nachfolgende Inkrementierung um Eins
Vorherige Dekrementierung um Eins
Nachfolgende Dekrementierung um Eins
Binäres Plus
Binäres Minus
Multiplikation
Division
Modulo (Rest mit Vorzeichen von i)
Zuweisung
i
i
i
i
i
=
=
=
=
=
i
i
i
i
i
*
/
%
+
-
3
3
3
3
3
Kleiner als
Größer als
Kleiner gleich
Größer gleich
Gleich
Ungleich
Linksschieben
Rechtsschieben
Bitweises UND
Bitweises XOR (Exklusives Oder)
+i
-i
++i
i++
--i
i-i + 2
i - 2
i * 2
i / 2
i % 13
i = 1
i *= 3
i /= 3
i %= 3
i += 3
i -= 3
i < j
i > j
i <= j
i >= j
i == j
i != j
i << 2
i >> 1
i & 7
i ^ 7
signed und unsigned sind sog. Modifizierer, da sie die Bedeutung des folgenden Begiffs ändern
111
Die Programmiersprache C++
|
~
<<=
>>=
&=
^=
|=
i | 7
~i
i <<= 3
i >>= 3
Bitweises ODER
Bitweise Negation
i = i << 3
i = i >> 3
i = i & 3
i = i ^ 3
i = i | 3
i &= 3
i ^= 3
i |= 3
Abb.: Operatoren für Ganzzahlen
C++ unterschiedet zunächst einmal zwei "integer"-Typen: char und int.
Zugehörige Werte sind ohne Umwandlung untereinander austauschbar.
Zeichenliterale (z.B. '!') sind "integer"-Werte. Auch Wahrheitswerte werden in
Ganzzahlen gefaßt. "0" entspricht dem logischen "false", alles andere ist "true".
Da der Datentyp char intern als 1-Byte-Ganzzahl dargestellt wird, sind eigentlich alle
Ganzzahl-Operatoren möglich. Bei der Interpretation von char als Zeichen sind nur
folgende Operatoren sinnvoll:
Operator
=
<
>
<=
>=
==
!=
Bedeutung
Zuweisung
Kleiner als
Groesser als
Kleiner gleich
Groesser gleich
Gleich
ungleich
Beispiel
x = ‘A‘
Der Datentyp size_t ist ein abgeleiteter Typ für Größenangeaben, die nicht negativ
werden können. Der Typ entspricht entweder unsigned int oder unsigned
long, je nach C++-System. Die Definition steht im Header <cstddef>.
Ein weiterer integraler Datentyp ist der Aufzählungstyp (enumeration type). Die
Syntax zur Deklaration ist
enum [Typname] {Aufzaehlung} [Variablenliste]90;
Bsp.:
enum farbtyp {rot,gruen,blau,gelb};
enum wochentag
{sonntag,montag,dienstag,mittwoch,donnerstag,freitag,samstag};
Falls der Datentyp bekannt ist, können Variable definiert werden, z.B.:
farbtyp einheitlich;
// Definition + Initialisierung
wochentag feiertag, werktag, heute=dienstag;
Wird ein Aufzählungstyp nur ein einziges Mal benötigt, kann der Typname
weggelassen werden. Man erhält dann eine anonyme Typdefinition, z.B.:
enum {fahrrad, mofa, lkw, pkw} fahrzeug;
90
Eckige Klammern bedeuten: Typname, Variablenliste können weggelassen werden. Sinnvoll ist meistens nur
das Weglassen der Variablenliste
112
Die Programmiersprache C++
Den mit Hilfe eines Aufzählungstyps definierten Variablen können anschließend
Werte aus der zugehörigen Liste zugewiesen werden. Aufzählungstypen sind eigene
Datentypen, werden intern aber auf natürliche Zahlen abgebildet.
Defaultmäßig erhält das erste Element in der Aufzählung den Wert 0 zugeordnet.
Jedes folgende Element erhält einen Wert zugeordnet, der um eine Einheit größer ist
als der Wert des unmittelbaren Vorgängers. Aufzählungen können mit einem Namen
versehen werden. Jede benannte Aufzählung definiert einen Typ und kann als TypSpezifizierer zur Deklaration von Bezeichnern verwendet werden, z.B. 91
// Demonstration zu Aufzaehlungstypen
#include <iostream.h>
enum Sprachen {ASSEMBLER,COBOL,C,CPP,FORTRAN,LISP,PASCAL,PROLOG} sprache;
int main()
{
bool allesistvergaenglich = true;
sprache = CPP;
// sprache ist vom Typ int
cout << "Sprache = " << int (sprache) << endl;
cout << "Alles ist vergaenglich = " << allesistvergaenglich << endl;
}
Umwandlungen von Enumerationen in int sind möglich, nicht möglich ist die
Umwandlung einer int-Zahl in einen enum-Typ. Als Operation auf enum-Typen ist
nur die Zuweisung erlaubt, bei allen anderen Operatoren wird vorher in int
umgewandelt.
Gleitpunktzahlen (Reelle Zahlen) verschiedener Größe werden beschrieben durch
float, double und double long.
Typ
float
double
long double
Bits
32
64
80
Zahlenbereich
Stellen, Genauigkeit
7
15
19
Intern werden Mantisse und Exponent jeweils durch Binärzahlen einer bestimmten
Bitbreite verkörpert.
Bsp.: Repräsentation reeller Zahlen (Anzahl Bits) im Rechner
Vorzeichen
Mantisse
Exp-Vorzeichen
Exponent
Summe
float
1
23
1
7
32
double
1
52
1
10
64
long double
1
64
1
14
80
Der Zahlenbereich wird wesentlich durch die Anzahl Bits für den Exponenten
bestimmt, der Einfluß der Mantisse ist minimal. Falls 32 Bits für die Darstellung einer
reellen Zahl verwendet werden, existieren nur 4.294.967.296 verschiedene
Möglichkeiten zur Bildung einer Zahl. Mit dem mathematischen Begriff eines reellen
Zahlenkontinuums hat das nur näherungsweise zu tun. Folgen der nicht exakten
Darstellung können sein:
91
PR21000.CPP
113
Die Programmiersprache C++
-
-
bei der Subtraktion zweier fast gleich großer Werte heben sich sie signifikanten Ziffern auf. Die
Differenz wird somit ungenau. Dieser Effekt ist unter dem Namen numerische Auslöschung
bekannt.
Die Division zu kleiner Werte ergibt einen Überlauf (overflow).
underflow tritt auf, wenn der Betrag des Ergebnisses zur Darstellung mit dem gegebenen Datentyp
zu klein ist. Das Resultat wird dann gleich 0 gesetzt.
Ergebnisse können von der Reihenfolge der Berechnung abhängen.
Die Zahlenbereiche für reelle Zahlen sind im Header <limits> festgelegt. Auskunft
über die Zahlenbereiche können über numeric_limits<float>- bzw.
numeric_limits<double>-Funktionen erhalten werden:
Operator
+
+
*
/
=
*=
/=
+=
-=
<
>
>=
<=
==
!=
Bedeutung
Unäres Plus
Unäres Minus
Binäres Plus
Binäres Minus
Multiplikation
Division
Zuweisung
d = d * 3
d = d / 3
d = d + 3
d = d - 3
Kleiner als
Grösser als
Grösser gleich
Kleiner gleich
Gleich
ungleich
Beispiel
+a
-a
a + 2
d
d
d
d
*=
/=
+=
-=
3
3
3
3
a != b
Abb.: Operatoren für float- und double-Zahlen
Bsp.: Berechnung mathematischer Ausdrücke
#include <iostream>
#include <cmath>
using namespace std;
int main()
{
float x;
cout << "x? "; cin
cout << "fabs(x) =
cout << "sqrt(x) =
cout << "sin(x) =
cout << "exp(x) =
cout << "log(x) =
}
>> x;
" << fabs(x) << endl;
" << sqrt(x) << endl;
" << sin(x) << endl; // x im Bogenmass
" << exp(x) << endl;
" << log(x) << endl;
Für negatives x erhält man z.B.:
x = 24
fabs(x) = 34
sqrt(x) = -NaN
sin(x) = -0.529083
exp(x) = 1.7.1391e-15
log(x) = -Infinity
114
Die Programmiersprache C++
Einige mathematische Funktionen (z.B. sqrt(), exp()) sind vordefiniert. Die
Deklarationen der meisten Funktionen befinden sich im Header <cmath>, die
Funktion abs() für int-Zahlen ist jedoch im Header <cstdlib> definiert.
Komplexe Zahlen bestehen aus dem Real- und Imaginärteil, die beide vom Typ
float, double bzw. long double sein können. Sie werden im Header
<complex> durch spezialisierte Templates realisiert. Mit komplexen Zahlen kann
wie mit reellen Zahlen gerechnet werden, z.B.:
#include <iostream>
#include <complex>
using namespace std;
int main()
{
complex<float> c1;
complex<float> c2(1.2, 3.4);
cout << c2 << endl;
c1 += c2;
c1 = c2 * 0.5f;
float re = c1.real();
cout << c1.imag() << endl;
}
// Komplexe Zahl 0.0 + 0.0i
// (1.2 + 3.4i)
// Standard-Ausgabeformat (1.2,3.4)
// Realteil ermitteln
// Imaginaerteil ausgeben
Neben den arithmetischen Operatoren für reelle Zahlen kann auch auf Gleichheit
(==) und Ungleichheit (!=) geprüft werden.
Mit dem Operator sizeof92 kann die Größe eines Datentyps bestimmt werden, z.B.93:
#include <iostream.h>
int main()
{
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
cout << "Groesse
<< " Bytes"
cout << "Groesse
<< " Bytes"
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
cout << "Groesse
<< endl;
}
92
93
von char .......... " << sizeof(char)
<< " Bytes"
von short ......... " << sizeof(short)
<< " Bytes"
von int ........... " << sizeof(int)
<< " Bytes"
von long .......... " << sizeof(long)
<< " Bytes"
von float ......... " << sizeof(float)
<< " Bytes"
von double ........ " << sizeof(double)
<< " Bytes"
von long double ... " << sizeof(long double) << " Bytes"
von unsigned short. " << sizeof(unsigned short)
<< endl;
von unsigned long.. " << sizeof(unsigned long)
<< endl;
von char* ......... " << sizeof(char *)
<< " Bytes"
von int* .......... " << sizeof(int *)
<< " Bytes"
von double* ....... " << sizeof(double *)
<< " Bytes"
Das Ergebnis von sizeof ist vom Typ unsigned int
pr21001.CPP
115
Die Programmiersprache C++
Der logische Datentyp bool wurde 1993 in den Standard aufgenommen. Vorher
wurde in C++ ein logischer Datentyp durch int simuliert. Dabei bedeutete ein Wert
ungleich 0 wahr (true) und ein Wert gleich 0 falsch (false). Zur Wahrung der
Kompatibilität wird der Datentyp bool an allen Stellen, die nicht ausdrücklich bool
verlangen, in int umgewandelt. Die umgekehrte Wandlung von int nach bool
ergibt false für 0 und true für alle anderen int-Werte. Die folgende Tabelle zeigt
Operatoren für logische Dateitypen:
Operator
!
&&
||
=
Bedeutung
Log. Negation
Log. Und
Log. Oder
Zuweisung
Bsp.
!i
a&&b
a||b
a = a && b;
Im Header <limits> wird die Template-Klasse numeric_limits definiert. Sie
umfaßt Spezialisierungen für Grunddatentypen: bool, char, signed char,
unsigned char, short, int, unsigned int, long, unsigned long, float,
double, long double. Für diese Grunddatentypen beschreiben Spezialisierungen
verschieden implementationsabhängige Funktionen und Eigenschaften, die alle
public sind.
Schnittstelle
bool is_specialised
T min()
T max()
int radix
int digits
int digits10
bool is_signed
bool is_integer
bool_is exact
T epsilon()
T round_error()
int min_exponent
int min_exponent10
int max_exponent
int max_exponent10
bool has_inifinity
T infinity()
T quiet_NaN()
T signalling_NaN
bool has_quiet_NaN
bool
has_signalling_NaN
bool is_iec559
bool is bounded
bool is_modulo
94
Bedeutung
true nur für Grunddatentypen, für die eine Spezialisierung vorliegt, false für
alle anderen.
minimal möglicher Wert
maximal möglicher Wert
Zahlenbasis, normal 2
Ganzzahlen: Anzahl Bits (ohne Vorzeichen-Bit)
Gleitkommazahlen: Anzahl der Bits in der Mantisse.
Annahme: radix == 2
Anzahl signifikanter Dezimalziffern bei Gleitkommanzahlen (z.B 6 bei float,
10 bei double)
true bei vorzeichenbehafteten Zahlen
true bei Ganzzahlentypen
true bei exakten Zahlen (z.B. ganzen Zahlen, rationale Zahlen)
Kleinster positiver Wert, für den die Maschine die Differenz zwischen 1.0 und
(1.0 + x) noch unterscheidet.
Maximaler Rundungsfehler
Kleinster negativer Exponent für Gleitpunktzahlen
Kleinster negativer 10er-Exponent für Gleitkommazahlen
Größtmöglicher Exponent für Gleitkommazahlen
Größtmöglicher 10er-Exponent für Gleitkommazahlen (>= +37)
true, falls der Zahltyp eine Repräsentation für „+Unendlich“ hat
Repräsentation von „+Unendlich“, falls vorhanden
Repräsentation einer „ruhigen NaN94“
Repräsentation einer signalisierenden NaN, falls vorhanden.
true, falls der Zahltyp eine nicht signalisierende Repräsentation für NaN hat
true, falls der Zahltyp eine signalisierende Repräsentation für NaN hat
true, falls der Zahltyp dem IEC 559 (= IEEE 754)-Standard genügt
true für alle Grunddatentypen, false wenn die Menge der darstellbaren Werte
unbegrenzt ist, z.B. bei Typen mit beliebiger Genauigkeit
true, falls bereichsüberschreitende Operationen wieder eine gültige Zahl
ergeben.
NaN: „not a number“
116
Die Programmiersprache C++
round_style
Art der Rundung
Ganzzahlen: rounded_toward_zero (= 0)
Gleitkommazahlen: round_to_nearest (= 1)
Abb.: <limits> Attribute und Funktionen
Mit typedef kann man einen anderen Namen für einen Typ vereinbaren, z.B.:
typedef float real;
int main()
{
real zahl = 1.7353;
....
}
// zahl ist vom Typ float
typedef eignet sich besonders zur Vereinfachung komplexer Datentypen.
Elementare Typen lassen sich beliebig mischen. Dabei werden Werte konvertiert. In
arithmetischen Ausdrücken mit binären Operatoren wird folgende implizite
Typkonvertierung vorgenommen: Der Operand mit dem niederwertigen Typ wird in
den höherwertigen Typ konvertiert. Jeder Operand vom Typ char und short wird
nach int und jeder Operand vom Typ float nach double konvertiert. Bei
Anweisungen wird versucht den Wert der rechten Seite in die linke Seite zu
konvertieren.
Wird ein vorzeichenbehaftetes Objekt (z.B. eine Variable) einem integer-Objekt
zugeordnet, erfolgt eine automatische Erweiterung des Vorzeichens. Objekte vom
Typ signed char haben immer ein Vorzeichen. Objekte vom Typ unsigned char
setzen das höherwetige Byte auf Null, wenn sie nach int konvertiert werden.
Bei Konvertierung eines längeren „integer“-Typs in einen kürzeren werden die
höherwertigen Bits abgeschnitten und die niederwertigen bleiben unverändert. Bei
der Konvertierung eines kürzeren „integer“-Typs in einen längeren wird das
Vorzeichen erweitert oder die zusätzlichen Bits des neuen Werts auf Null gesetzt.
Das hängt davon ab, ob der kürzere Typ signed oder unsigned ist.
Typ
char
unsigned char
signed char
short
unsigned short
enum
Konvertierung in
int
int
int
int
unsigned int
int
Verfahren
Null- oder Vorzeichen erweitert
Höherwetige Bits immer Null
Vorzeichen erweitert
unverändert, Vorzeichen erweitert
unverändert, nullerweitert
unverändert
Abb.2.1-6: Verfahrensweise bei arithmetischen Standarkonvertierungen
Eine explizite Typenkonvertierung (type cast)95 wird durch (typ) ausdruck
96veranlaßt. In C++ ist auch typ (ausdruck) möglich97, z.B.:
int i = 5;
double d;
d = (double) i; bzw. d = double (i)
95
Die explizite Konvertierung bezeichnet man als cast
Aus der Programmiersprache C übernommene Schreibweise
97 Funktionsschreibweise
96
117
Die Programmiersprache C++
Die Funktionsschreibweise kann nur für Typen verwendet werden, die einen Namen
haben. Soll bspw. ein ganzzahliger Wert in eine Adresse konvertiert werden, dann
muß die Konvertierung in "cast"-Schreibweise angegeben werden, z.B.:
char *z = (char *) 0777;
Die aus C übernommene Schreibweise ist auch für komplexe Datentypen möglich.
Nachteile von "Casts" im C-Stil können mit Hilfe von neuen Typumwandlungsfunktionen vermieden werden. Sie arbeiten nach folgendem Schema:
operatorname<T>(ausdruck)
Das Ergebnis des Ausdrucks soll in den Typ T umgewandelt werden. Der
static_cast-Operator ist zur Durchführung (bzw. zum Rückgängig-Machen) von
Standardtypumwandlungen vorgesehen.
1. Bsp.: Umwandlung vom Enumerationstyp in einen ganzzahligen Zahlenwert
enum wochentag {sonntag, montag, dienstag, mittwoch} heute = dienstag;
int i = dienstag; // implizite Typumwandlung nach int
heute = i; // Fehler, Datentyp inkompatibel
heute = static_cast <wochentag>(i); // erlaubt!
2. Bsp.: Umwandlungen von Zeichen in ganze Zahlen
Die Konvertierung geschieht über den static_cast-Operator (mit Angabe des
gewünschten Datentyps in spitzen und Angabe der Variable in runden Klammern).
Man kann die Umwandlung so beschreiben:
char zch;
int i;
i = static_cast<int>(zch);
// Umwandlung char in int
Es gelten die Identitäten
zch == static_cast<char>(static_cast<int>(zch));
i == static_cast<int>(static_cast<char>(i));
, falls –128 <= zch <= 127 ist (bzw. 0 <= i <= 255 bei unsigned char).
Liegt i außerhalb dieses Bereichs, gibt es einen Datenverlust, weil die überflüssigen
Bits bei der Umwandlung nicht berücksichtigt werden können.
Der dynamic_cast-Operator dynamic_cast<T>(a) wirkt ähnlich, zeigt aber
folgende Unterschiede:
-
Die Typüberprüfung findet zur Laufzeit statt, falls das Ergebnis nicht schon zur Compilier-Zeit
bestimmt werden kann.
Der Typ T muß ein Zeiger oder eine Referenz auf eine Klasse sein.
Falls das Argument a ein Zeiger ist, der nicht auf ein Objekt vom Typ T (oder abgeleitet von T)
zeigt, wird das Ergenis NULL zurückgegeben.
Falls das Argument a eine Referenz ist, die nicht auf T (oder abgeleitet von T) verweist, wird eine
Ausnahme oder Exception vom Typ bad_cast ausgeworfen.
Ein reinterpret_cast reinterpret_cast<T>(a) kann einen Zeigertyp in einen
anderen, Zahlen in Zeiger und umgekehrt Zeiger in Zahlen umwandeln. Es gibt nur
einen sicheren Verwendungszweck für das Ergebnis eines reinterpret_cast: die
Typumwandlung zurück in den ursprünglichen Typ.
118
Die Programmiersprache C++
Mit dem const_cast-Operator const_cast<T>(a) können die Attribute const,
volatile und __unaligned einer Klasse entfernt werden.
Bsp.:
const int i = 100;
const int *ip = &i;
*ip = 0; // geht nicht
int *iq = const_cast<int *>(&i); // explizite Typumwandlung
*iq = 0;
Initialisierer setzen den Anfangswert, der in einem Objekt (Variable, Array, Struktur)
gespeichert wird. Ist das Objekt von automatischer Lebensdauer, dann ist sein Wert
unbestimmt. Ein nicht initialisiertes Objekt statischer Lebensdauer wird durch die
Voreinstellung folgendermaßen initialisiert:
-
Es wird auf Null gesetzt, falls es zu einem arithmetischen Typ gehört
Es wird auf Null gesetzt, falls es ein Zeigertyp ist.
119
Die Programmiersprache C++
2.2 Abgeleitete Datentypen
Sie entstehen durch Anwendung bestimmter Konstruktionsvorschriften auf bekannte
(fundamentale oder abgeleitete) Datentypen. Es entstehen dadurch Konstante,
Zeiger, Referenzen, Felder (arrays), Strukturen, Variantenstrukturen, Funktionen und
Klassen.
2.2.1 Konstanten
C++ unterscheidet:
a) Integer-Konstanten
Die kommen in 4 verschiedenen Formen vor: dezimal, oktal (Kennzeichen:
vorangestellte 0), hexadezimal (Kennzeichen durch 0x bzw. 0X am Anfang) und als
"character"-Konstanten (z.B. char z = 10)
Steht am Ende einer Konstante ein Buchstabe L (bzw. l), so handelt es sich um eine
Zahl vom Typ long, bei U (bzw. u) ist die Zahl vom Typ unsigned, z.B.
1311u
1311l
// unsigned
// long
b) Zeichen-Konstanten
Eine "character"-Konstante ist in Hochkommata eingeschlossen, z.B. 'a', 'b',
'1', '\n'98.
Eine besondere Art von Zeichenkonstanten sind Escape-Sequenzen zur Darstellung
von nicht abdruckbaren Zeichen. Sie werden durch „\“ eingeleitet, dem ein
Buchstabe, ein Oktal- oder Hexadezimal-Wert folgt. Intern werden sie als Zeichen
vom Typ char gespeichert.
‘\n’
‘\r’
‘\t’
‘\v’
‘\b’
‘\f’
‘\\’
‘\’’
‘\“’
‘\a’
Neue Zeile
Wagenrücklauf (Carriage Return)
Horizontaler Tabulator
Vertikaler Tabulator
Backspace
Neue Seite (Formfeed)
Backslash
Apostroph
Anführungszeichen
Alarm
Zusätzlich gibt es „lange“ Zeichen (wide characters) vom Typ wchar_t. „Wide
Characters“ sind für Zeichensätze gedacht, bei denen ein Byte nicht zur Darstellung
eines Zeichens ausreicht.
Neben diesen „Ein-Zeichen-Konstanten“ sind auch Konstanten mit 2 Zeichen möglich (‘ab’,’XY’). Diese
stellen „integer“-Werte dar. Man sollte auf sie jedoch verzichten und in diesen Fällen eine hexadezimale
Schreibweise bevorzugen.
98
120
Die Programmiersprache C++
c) Reelle Konstanten (Gleitpunkt-)
Sie sind vom Typ double und können sowohl in üblicher Gleitpunktdarstellung als
auch in exponentieller Form angegeben werden, z.B.:
Darstellung
13.
-.4
-13.0e-1
-04.E+2
13E-2
1.311E1
Wert
13,0
-0,4
-1,3
-400,0
0,13
13,11
Auch Gleitpunktzahlen können durch Anhängen eines Suffix zu einem bestimmten
Typ gemacht werden. So bedeutet f (oder F) float und l (oder L) long double.
Bsp.: Demonstration von Länge und Typ einzelner Gleitpunktkonstanten
#include <iostream.h>
void main()
{
cout << "\n" << sizeof(13.);
cout << "\n" << sizeof(13.f);
cout << "\n" << sizeof(13.l);
}
Zum Beschreiben einer Gleitpunktzahl ist der Dezimalpunkt unbedingt erforderlich,
z.B. 1311. anstatt 1311 (wird als „int“ betrachtet).
d) Zeichenketten (Strings)
Eine Zeichenkette ist eine Zeichenfolge, die durch doppelte Hochkommata
eingeschlossen ist, z.B.:
"Hallo Semester I5T"
Jede Zeichenkette enthält zusätzlich das Zeichen '\0', das das Ende der
Zeichenkette
bestimmt.
Außerdem
kann
jedes C++-Zeichenkettenliteral
Kontrollzeichen im Text enthalten, die jeweils durch einen Backslash (\) eingeleitet
sind. Die Länge einer Zeichenkette kann nach ANSI-Norm bis zu 509 Zeichen
betragen. Die meisten Compiler erlauben jedoch größere Werte (z.B. 2048 Bytes).
e) const
An diesem Schlüsselwort erkennt C++ Konstanten, z.B.
const
const
const
const
int pufferGroesse = 512;
int zaehler
= 50;
double pi;
// Fehler, nicht initialisiert
float pi
= 3.14159;
So definierte symbolische Konstante sind Variable, die nicht verändert werden
dürfen. "zaehler = 50;" führt bspw. auf einen Fehler, da "zaehler" eine
Konstante ist.
121
Die Programmiersprache C++
Bsp.: "Demonstation symbolischer Konstanten99
#include <iostream.h>
// Zeichenkonstante
const char zeichen
= '&';
// Stringkonstante
const char zeichenkette[] = "Hallo Informatik! ";
// Integerkonstanten koennen dezimale, oktale oder hexadezimale Zahlen sein
const int oktal
= 0233;
const int hexaD
= 0x9b;
const int dezimal
= 155;
// Gleitpunktkonstante
const float gleitpunkt
= 3.1459;
void main()
{
cout << zeichen
cout << zeichenkette
cout << oktal
cout << hexaD
cout << dezimal
cout << gleitpunkt
}
<<
<<
<<
<<
<<
<<
endl;
endl;
endl;
endl;
endl;
endl;
Einer mit const deklarierten Konstanten ordnet der Compiler die Speicherklasse
static zu.
Das Gegenstück zum Spezifizierer const ist volatile. Über dieses Schlüsselwort soll
dem Compiler mitgeteilt werden, daß das Objekt von außen (z.B. über eine InterruptRoutine) verändert werden kann. Der Programmcode, der das über volatile
spezifizierte Objekt enthält, sollte deshalb nicht vom Compiler optimiert werden. Die
Deklaration eines Objekts als volatile warnt den Compiler davor, Annahmen über
den Wert des betreffenden Objekts anzustellen, weill sich dieser (theoretisch)
ständig ändern kann. Mit volatile deklarierte Variablen werden nicht als
Registervariable behandelt, z.B.100:
#include <iostream.h>
void benutzeregister(void);
void benutzevolatile(void);
main()
{
benutzeregister();
cout << endl;
benutzevolatile();
}
void benutzeregister(void)
{
register int k;
cout << "Zaehlen mit Registervariablen " << endl;
for (k = 1; k <= 100; k++) cout << k;
}
void benutzevolatile(void)
{
volatile int k;
cout << "Zaehlen mit einer Volatilevariablen " << endl;
for (k = 1; k <= 100; k++) cout << k;
99
PR22101.CPP
PR22104.CPP
100
122
Die Programmiersprache C++
}
2.2.2 Zeiger (pointer types)
Definition
Zeigertypen sind Datentypen zur Manipulation von Objektadressen. Ein Zeigertyp
entsteht, indem vor dem deklarierten Namen ein * (Stern)101 angegeben wird, z.B.:
int* zgr;
// zgr ist ein Zeiger auf ein int-Objekt
Für viele Typen T ist T* "pointer" auf T. Eine Variable vom Typ T* kann eine
Adresse vom Typ T enthalten So teilt int *zgr bzw. int* zgr dem Compiler mit,
daß die Variable zgr "Zeiger" aufnehmen kann. Zeiger sind typgebunden.
Zur Zuordnung der Adresse eines Objekts kann der Adreßoperator & benutzt
werden, z.B.:
int* zgr;
int i = 3;
zgr = &i;
3
zgr
i
Abb. 2.2-1: Ein Zeiger auf das ganzzahlige Objekt i
"zgr" hat als Wert die Adresse der Variablen i zugewiesen bekommen. Der Inhalt
der Variablen i kann über zgr durch Zeigerderefenzierung102 angesprochen
werden.
*zgr = *zgr +1;
// aequivalent zu i = i + 1;
Das Zeichen * hat im Zusammenhang mit Zeigern 2 Bedeutungen:
1. Bei der Definition kennzeichnet es eine Variable als Zeigervariable
2. Innerhalb des Programmablaufs (in Anweisungen) zeigt es an, daß der Inhalt der Speicherstelle
gemeint ist, auf die der Zeiger verweist (Dereferenzierung).
101
Der Stern vor dem Namen gehört aber nicht zum Namen sondern zum Typ
Inhalts- bzw. indirection-Operator. Welche Bedeutung „*“ hat, ermittelt C++ aus dem Zusammenhang
(Kontext), in dem „*“ auftritt
102
123
Die Programmiersprache C++
Referenz
"T& name" bedeutet: Das so definierte Objekt ist eine Referenz auf das Objekt vom
Typ T.
Der Operator & liefert eine Adresse eines Objekts (Referenz). Mit dem Operator *
erhält man wieder den Wert des Objekts (dereferenzieren). "*" wird aber auch bei
der Deklaration bzw. Definition von Zeigervariablen benutzt.
Referenztypen sind eng mit Zeigertypen verwandt.
Bsp.:
1. Darstellung von Zeigern, Referenzen, Dereferenzierung103
#include <iostream.h>
void main()
{
int i
= 1024;
int *zgri = &i;
cout << "i: " << i << "\t\t&i:\t" << &i << endl;
cout << "*zgri: "
<< *zgri
<< "\tzgri:\t"
<< zgri << endl
<< "\t\t&zgri:\t" << &zgri << endl;
}
Die Ausgabe dieses Programms zeigt:
i: 1024
*zgri: 1024
&i:
0x2e972240
zgri: 0x2e972240
&zgri: 0x2e97223c
2. Veranschaulichung von Zeigerstrukturen
int i = 2, j = 3;
int *zi, **zzi;
zi = &i
2
zi
i
3
zzi
j
*zi = 3
3
zi
i
zzi = &zi;
3
zzi
103
j
PR22304.CPP
124
Die Programmiersprache C++
4
zi
i
3
zzi
j
zi
i
*zzi = &j
zzi
j
Abb. 2.2-2: Veranschaulichung von Zeigerstrukturen
3. Ein Demonstrationsprogramm , das das Kreieren, Initialisieren und Dereferenzieren einer "Pointer"-Variablen zeigt.
#include <iostream.h>
char z;
int main()
{
char *zz;
// Zeiger zu einer Zeichenvariablen
zz = &z;
for (z = 'A'; z <= 'Z'; z++) cout << *zz;
}
Null-Pointer
Numerische Zeigerkonstanten sind unüblich. Eine Ausnahme davon ist der
Nullzeiger (üblicherweise ist das eine Codierung für "Adresse noch undefiniert"). In
vielen Programmen wird dafür eine Präprozessorkonstante NULL (definiert im
Header <cstddef>) benutzt. Allerdings ordnet auch die Zuweisung der Konstanten
0 einem Zeiger den Null-Zeigerwert zu. In C++ wird NULL als Zahlenwert 0 oder 0L
dargestellt, so daß in einem C++-Programm der Name NULL nicht notwendig ist.
Zeigerarithmetik
Für Zeigertypen ist eine besondere (Zeiger-)Arithmetik definiert. Zu einem
Adreßausdruck darf ein integraler Ausdruck (vom Typ char, short, int, long, ...)
addiert bzw. subtrahiert werden. Arithmetische Operationen berücksichtigen
automatisch die Größe des Typs.
Angenommen wird, daß "x" und "y" Zeigerausdrücke sind, "i" ein ganzzahliger
Ausdruck ist und "var" eine Variable vom Typ T ist.
Adresse
&
zgr = &var
weist die Adressse von "var" an "zgr"
Zuweisung
125
Die Programmiersprache C++
=
zgr = x
Dereferenzieren
*
var = *zgr
weist den Zeigerwert "x" der Variablen "var" zu.
weist das Datenelement vom Typ T, das durch "zgr"
refernziert wird, der Variablen "var" zu
Dynamische Speicherbelegung und Freigabe
new
zgr = new T erzeugt dynamisch Speicher für ein Datenelement vom
Typ T und weist die Adresse dieses Speichers "zgr"
zu
delete delete zgr löscht den dynamischen Speicherbereich der durch
die
Adresse "zgr" lokalisiert wird.
Arithmetik
+
x + i
zeigt auf das Datenelement, das "i" Datenelemente
rechts vom durch "x" adressierten Element liegt.
x - i
zeigt auf das Datenelement, das "i" Datenelemente
links vom durch "x" adressierten Element liegt.
x - y
gibt die Anzahl der Datenelemente vom Basistyp
zurück, die zwischen den beiden Zeigern liegen
Vergleichen (relationale Operationen)
Die 6 standardmäßig vorliegenden relationalen Operatoren können auf
Zeiger angewendet werden. Vergleiche beziehen sich auf ganzzahlige Werte
ohne Vorzeichen ("unsigned int")
Abb. 2.2-10: Operationen zu Zeigerausdrücken
Zeigerkonvertierungen
Zeigertypen können im Rahmen der Typumwandlung in andere Zeiger konvertiert
werden, z.B.:
char* zeichenkette;
int* iz;
zeichenkette = (char*) iz;
Allgemein kann man das so ausdrücken: (T*) konvertiert einen Zeiger in den Typ
Zeiger auf T.
Dynamische Speicherbelegung und Freigabe
Da Zeiger selbst Objekte sind, können Zeiger auch auf Zeiger zeigen. Felder
(arrays), Strukturen und Varianten, Konstante und Klassen sind Objekte, auf die
gewöhnlich gezeigt wird. Speicherplatz für derartige Objekte mit dynamischer Dauer
kann man mit dem C++-Operator new besorgen und mit delete wieder freigeben.
Kann kein Speicherplatz bereitgestellt werden, dann wird NULL zurückgeliefert.
new new_Argumente Typname Initialisierer
new new_Argumente (Typname) Initialisierer
new_Argumente läßt sich zur Übergabe weiterer Argumente an new verwenden.
Dazu ist allerdings eine überladene Version von new erforderlich, die mit den
optionalen Argumenten übereinstimmt. Initialisierer wird (falls angegeben) zur
Initialisierung der Reservierung von Speicherplatz verwendet. new versucht ein
Objekt des Typs Typname durch Reservierung von sizeof(Type) Bytes im freien
126
Die Programmiersprache C++
Speicher (dem Heap) zu erstellen. Der Operator new[] dient zur Erzeugung von CArrays.
Der zurückgegebenen Zeiger hat immer den Typ „Zeiger auf Type“. Das neue Objekt
wird von seiner Erzeugung bis zu dem Moment gespeichert, in dem der Operator
delete es löscht.
delete Typumwandlungsausdruck
delete [] Typumwandlungsausdruck
Bsp.:
char* z = new char[100];
// Platz für 100 Zeichen
int* i = new int;
// Platz für eine Zahl
/* Die Anforderung von Speicherplatz erfolgt durch Angabe
des Objekttyps, das System liefert daraufhin die Startadresse
des Speicherplatzes zurück */
int* dynfeld;
int groesse;
/* groesse wird belegt */
.........
dynfeld = new int[groesse];
/* "new int[groesse]" liefert einen Zeiger auf einen
dem Heap entnommenen Speicherblock der Groesse
"groesse * sizeof(int)"
*/
Mit delete wird Speicherplatz freigegeben, z.B.
delete[100] z;
delete i;
delete [] dynfeld;
/* Die Freigabe von Speicherplatz geschieht durch Angabe
der Speicherplatz-adresse und evtl. der Anzahl der Objekte,
die gelöscht werden sollen */
void-Pointer
Mit dem speziellen Typ void* werden Zeiger definiert, die auf Objekte eines zur
Compilierungszeit unbekannten Typs verweisen. C++ muß dann zur Ablaufzeit
wissen, welchen Datentyp ein void-Zeiger adressiert. Das erfordert einen "pointer
type cast"-Ausdruck, der temporär einen Zeiger zu einem Datentyp bindet.
Bsp104.:
#include <iostream.h>
void ausgabeZeiger(void *zgr);
int main()
{
char puffer[100];
void *zpuffer;
// zpuffer = (void *) &puffer[0];
zpuffer = &puffer;
* (char *) zpuffer = 'A'; // Einspeichern eines Zeichens ueber den Puffer
puffer[1] = 'B';
// direktes Einspeichern
puffer[2] = 'C';
puffer[3] = '\0';
cout << "Adresse von Puffer = ";
ausgabeZeiger(&puffer);
104
vgl. PR22202.CPP
127
Die Programmiersprache C++
cout << "Daten im Puffer = ";
cout << (char *) zpuffer;
}
void ausgabeZeiger(void *zgr)
{
cout << hex << zgr << endl;
}
Zeiger auf Konstante bzw. konstante Zeiger
const-Objekte können über Zeiger nicht indirekt verändert werden, z.B.:
const double pi = 3.14;
double* zgr = &pi:
/* ist verboten, da &pi den typ "Zeiger auf eine Konstante besitzt */
Der Fehler kann behoben werden, z.B.:
const double* zgr = π
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
128
Die Programmiersprache C++
Zeiger auf Funktionen
Ist die Adresse einer Funktion bestimmt, dann kannn über diesen "Pointer" die
Funktion aufgerufen werden. Der Typ „ist Zeiger auf eine Funktion“ besitzt folgende
syntaktische Form:
rückgabewertTyp (*funk) (argumentenTyp1, argumentenTyp2, ...)
Rechnungen mit Zeigern auf Funktionen sind nicht zulässig.
Bsp.: Sortieren der Elemente in Arbeitsspeicherfeldern
1. Zum Sortieren muß eine passende Vergleichsfunktion gefunden werden. Die
Zuordnung der passenden Vergleichsfunktion erfolgt zweckmäßig über eine
Funktions-Pointer. Dafür wird ein Typname (mit Argumentenliste) deklariert (zur
Erleichterung der Beschreibung der etwas ungewöhnlichen Syntax).
typedef int (*VRGL) (int,int);
void tauschen(int* x, int i, int j)
{
int zwischen = x[i];
x[i] = x[j];
x[j] = zwischen;
}
int vrgl1(int z1, int z2)
{
return z1 - z2;
}
int vrgl2(int z1, int z2)
{
return z2 - z1;
}
void bsort(int* x, int n, VRGL vrgl)
// void bsort(int*x, int n, int (*)(int,int) vrgl)
{
// Sortieren durch Austauschen
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if ((*vrgl)(x[i],x[j]) < 0)
tauschen(x,i,j);
}
2. Zum Lieferumfang zahlreicher C++-Compiler gehört eine Bibliotheksfunktion
void qsort(void* zgrVornF, size_t anz, size_t bytes,
int (*fcompare)(const void* elem1, const void* elem2);
zgrVornF: Zeiger auf das erste Element
anz: Das Feld besteht aus anz Elementen
bytes: Größe der Elemente
Wie sortiert wird, bestimmt die Funktion fcompare(). Sie erwartet zwei Zeiger auf
ein beliebiges Element als Argument und muß zurückgeben:
129
Die Programmiersprache C++
0,
falls *elem1 == *elem2
< 0, falls *elem1 < *elem2
> 0, falls *elem1 > *elem2
Der Typ size_t ist ein typedef für unsigned int.
qsort() wird mit der Funktionsprozedur sortieren() aufgerufen, die ein
Zeichenkettenfeld (char* [ ]) zum Sortieren der Zeichenkettenelemente übergibt.
#include <stdlib.h>
#include <string.h>
void sortieren(char* x[], int n)
{
int vergleiche(const char*[], const char*[]);
qsort((void*) x, (size_t) n, (size_t) sizeof(x[0]),
(int (*) (const void*, const void*)) vergleiche);
/* explizite Typumwandlung. In qsort() sind Zeiger auf void
verlangt */
}
int vergleiche(const char*elem1[], const char* elem2[])
{
return(strcmp(*elem1,*elem2));
}
2.2.3 Vektoren (Arrays)
2.2.3.1 C-Arrays
Definition und Initialisieren
Vektoren bestehen aus einem zusammenhängenden Feld von einzelnen Variablen
gleichen Namens und gleichen Typs, die über einen ganzzahligen
Index
angesprochen werden können. Bspw. ist int feld[10] eine Vektordefintion. Die
von [ .. ] eingeschlossene Ganzzahl bestimmt die Anzahl der Vektorelemente. Hier
sind 10 Variable feld[0], feld[1], ... , feld[9] vom Typ int verfügbar.
Die Elemente werden vom Anfangsindex 0 aus durchgehend indiziert.
Vektorelemente werden dadurch ausgewählt, daß dem Vektornamen, in [..]
eingeschlossen, ein Ausdruck folgt, der eine ganze Zahl liefert. Vektorelemente
können vordefiniert werden, z.B.:
int feld[10] = {1,2,3,4,5,6,7,8,9,10};
Bei reinen Deklarationen kann die Ausdehnungsangabe entfallen, z.B.:
extern double v[];
Dies gilt auch für den Fall der Definition mit Initialisierung durch Angabe einer
Werteliste, z.B.:
double x[3] = {1,0,0};
double y[3] = {0,1,0};
double z[] = {0,0,1};
130
Die Programmiersprache C++
Parameterübergabe
Vektoren (arrays) werden in C++ immer mit call by reference übergeben, z.B.
void ausgabe(int[])
wird vom Compiler so behandelt: void ausgabe(int *)
Die Größe des Felds spielt bei der Deklaration der formalen Parameter keine Rolle.
Die folgenden drei Deklarationen sind äquivalent:
void ausgabe(int *)
void ausgabe(int [])
void ausgabe(int[10])
C-Arrays und sizeof
C_Arrays sind keine Objekte (Instanzen) und besitzen auch keine Methoden. Die
Größe eines C-Array muß vorher bekannt sein oder mit sizeof ermittelt ermittelt
werden, z.B.:
const int anzahl = 5;
int tablelle[10];
for (int i = 0; i < anzahl; i++)
cout << i << ": " << tabelle[i] << endl;
// bzw.
for (int i = 0; i < sizeof(tabelle)/sizeof(tabelle[0]); i++)
cout << i << ": " << tabelle[i] << endl;
sizeof() gibt den Platzbedarf des in Klammern stehenden Objekts in Bytes
zurück. Die Anzahl ergibt sich durch Division des Speicherbedarfs für das ganze
Feld durch den Platzbedarf für ein einzelnes Element. Falls der Datentyp eindeutig
bekannt ist, kann anstatt sizeof(tabelle[0]) auch sizeof(int) geschrieben
werden.
C-Zeichenketten
Eine C-Zeichenkette (string) ist ein Spezialfall eines Arrays. C++ Zeichenketten kann
man so
char s[] = {'s','t','r','i','n','g','\0'}
oder einfacher so
char s[] = "string".
definieren. Der Übersetzer berechnet die
Wortlänge anhand der Zahl der
Initialisierungen. Bei s wird sie hier auf 7 festgelegt, da ein "string" immer mit dem
"\0"-Zeichen endet. Derartige Zeichenketten sind die einzige Form von C-Arrays, die
auf einmal ausgegeben werden können, z.B.:
cout << s;
131
Die Programmiersprache C++
Ebenso ist es möglich, eine Zeichenkette über >> und cin auf einmal zu füllen:
cin >> s;
Mit der Benutzung der binären Null als Endemarkierung kann man theoretisch
beliebig lange Zeichenketten erzeugen. Mit
s[0] = '\0';
ist die Länge der Zeichenkette auf Null gekürzt, also gelöscht. Zur Bearbeitung von
Zeichenketten befinden sich in der Header-Datei105 string.h drei Funktionen
strcpy (kopiert eine Zeichenkette, hängt automatisch binäre Null ans Ende der
Kopie), strlen ( ermittelt die Länge einer Zeichenkette) und strcat (hängt eine
Zeichenkette an eine andere an).
Bei der Arbeit mit Zeichenketten ist, genau wie bei anderen Vektoren (arrays) darauf
zu achten, daß das aufzunehmende Feld groß genug ist. C++ überprüft die Grenzen
eines Felds nicht und schreibt daher über diesen Bereich hinaus.
Funktionen zur Manipulation von Zeichenketten
Sie sind aus der C-Standardbibliothek übernommen und in der Header-Datei
string.h deklariert. Diese Routinen lassen sich in 3 Gruppen unterteilen. Die 1.
Gruppe behandelt nullterminierte Zeichenketten. Die Namen dieser Funktionen
beginnen mit str. Die 2. Gruppe sind die strn-Funktionen. Die beenden ihre Arbeit,
sobald das erste Nullzeichen auftritt, bzw. nach spätestens n (zusätzliches
Argument) Zeichen. Die Funktionen der 3. Gruppe (mem-Funktionen) operieren auf
Bytefeldern bekannter Länge und beachten das Nullzeichen nicht.
-cpy()-Funktionen
Sie kopieren den Inhalt von s auf den Inhalt von t. „strcpy()“ und „strncpy()“
bis jeweils zum ersten Nullbyte von s (einschließlich), strncpy() jedoch maximal n
Zeichen. „memcpy()“kopiert genau n Bytes.
char* strcpy(char* t, const char* s);
char* strncpy(char* t, const char* s, size_t n);
void* memcpy(void* t, const void* s, size_t n);
-cat()-Funktionen
Sie ermitteln das Ende der Zeichenkette t und fügen dort die Zeichenkette s an. Sie
geben t zurück.
char* strcat(char* t, const char* s);
char* strncat(char* t, const char* s, size_t n)
-cmp()-Funktionen
Sie vergleichen die beiden ersten Argumente. Sie liefern -1 zurück, falls a < b106
gilt, 0 für a = b und +1 für a > b.
105
106
Header <cstring>
Die Relationen < bzw > bezeichnen hier die lexikalische Reihenfolge
132
Die Programmiersprache C++
int strcmp(const char* a, const char* b);
int strncmp(const char* a, const char* b, size_t n);
int memcmp(const void* a, const void* b, size_t n);
strchr() und memchr() geben einen Zeiger auf das erste, strrchr() einen
Zeiger auf das letzte Auftreten des Zeichen z zurück. Wird z in s nicht gefunden,
wird 0 zurückgegeben.
char* strchr(const char* s, int z);
void* memchr(const void* s, int z, size_t n);
char* strrchr(const char* s, int z);
strpbrk() liefert einen Zeiger auf das erste Auftreten irgendeines Zeichen aus der
Zeichenkette p in der Zeichenkette s. Bei Mißerfolg wird 0 zurückgegeben.
char* strstr(const char* s, const char* p);
strstr() liefert einen Zeiger auf das erste Auftreten der Zeichenkette p als
Teilstring der Zeichenkette s. Enthält s den String p nicht, so ist das Ergebnis 0.
char* strstr(const char* s, const char* p);
char-Arrays
Genau wie bei C-Arrays wird für char-Arrays Speicherplatz zur Kompilierungszeit
reserviert. „char“-Arrays können auch keine L-Werte sein. Wie bei Strings (char *)
nimmt der Ausgabeoperator << an, daß ein char-Array mit ‘\0‘ abschließt. Die
Initialisierung kann wie bei Strings geschehen oder wie bei C-Arrays vorgenommen
werden.
Zeiger und Feldnamen
Feldnamen sind konstante Zeiger auf das 1. Feldelement, z.B.: &s[0]. Eine
Dereferenzierung ermöglicht den Zugriff. So kann
*s
*(s+1)
geschrieben werden anstatt s[0]
geschrieben werden anstatt s[1].
Bsp.: Gegeben ist ein Vektor, der Ganzzahlen aufnehmen kann.
int x[5];
Der Zeiger int *zx; zeigt nach der Zuweisung zx = &x[0]; auf das 1. Element
des Vektors. "zx + 1" zeigt dann auf x[1]. Das Inkrementieren eines Zeigers um
Eins bewirkt, daß der Zeiger auf das nächste Objekt dieses Typs zeigt. Statt x[1]
kann man auch *(zx + 1) schreiben. Da in C++ der Name eines Vektors
gleichbedeutend mit einem Zeiger auf seinen Anfang ist, läßt sich zx = &x[0];
auch einfach durch zx = x;ersetzen. Allerdings besteht ein Unterschied: "zx" ist
eine Zeigervariable, x eine Zeigerkonstante. So ist "zx + 1", "zx + 3" erlaubt. Dies
ist aber nur für "x" nicht erlaubt, "(*x)++" ist zur Inkrementierung von x[0] um 1
erlaubt.
133
Die Programmiersprache C++
x[0]
zx
x[1]
x[2]
zx+2
x [3]
x[4]
zx+4
Abb. 2.2-3: Zeigerarithmetik
Elementweises Kopieren von Vektoren
Felder (C-Arrays) können nur elementweise in C++ kopiert werden. So ist
char t[7];
t = s;
falsch. Der Compiler kennt diesen Fehler jedoch nicht, denn er interpretiert diese
Anweisung als „Kopieren einer Zeigervariablen“. Da es keine Zuweisungen eines
Felds an ein anderes gibt, kann auch nicht über die return()-Anweisung ein Feld
als Funktionsrückgabewert erscheinen.
Mehrdimensionale Felder
Sie werden folgendermaßen definiert:
int matrix[10][10]
Diese Definition erzeugt die Elemente matrix[i][j] für 0 <= i , j <= 9. Die
Grenzen von Feldern sind konstant und werden zur Übersetzungszeit festgelegt.
Bsp.:
char v[2][5] = {'a','b','c','d','e',
'1','2','3','4','5'}
// erster Vektor
// zweiter Vektor
kann auch so angegeben werden
char v[2][5] = {{'a','b','c','d','e'},
{'1','2','3','4','5'}}
// erster Vektor
// zweiter Vektor
Der Zugriff auf die Komponenten erfolgt über Angabe der entsprechenden Indizes,
z.B.
v[0][0]
v[1][2]
// verweist auf den Wert 'a'
// verweist auf den Wert '3'
134
Die Programmiersprache C++
Feld[0]
Feld[0][0]
Feld[0][1]
Feld[1]
Feld[1][0]
Feld[1][1]
Feld[1][2]
Feld[2][0]
Feld[2][1]
Feld[2][2]
Feld[2]
Feld[0...2][0]
Feld[0][2]
Feld[0...2][1] Feld[0...2][2]
Abb. 2.2-4: Ein zweidimensionales Feld
Elemente eines zweidimensionalen Felds werden zeilenweise im Speicher abgelegt:
Feld[0][0]
Feld[0][1]
Feld[0][2]
Feld[1][0]
.....
Allgemein kann festgehalten werden: Der letzte Index läuft immer am schnellsten.
Auch mehrdimensionale Felder können direkt bei der Deklaration Werte zugewiesen
bekommen.
Mehrdimensionale Vektoren lassen sich dadurch simulieren, daß anstelle von
Vektoren auf Vektoren ersatzweise "Vektoren von Zeigern auf Vektoren" definiert
werden, z.B.:
char* z[3] = {"1.Zeile","2.Zeile","3.Zeile"};
[0]
1 . Ze
i l e \0
[1]
2 . Ze
i l e \0
[2]
3
. Ze
i l
e \0
Abb. 2.2-5: Vektor mit Zeigern auf Zeichenketten
Bsp.: „Sortieren durch Austauschen“ von Zeichenketten
-
Aufbau eines mehrdimensionalen Vektors zur Verwalltung von Zeichenketten.
135
Die Programmiersprache C++
Die Zeichenketten werden in einem Wortspeicher gebracht, der folgendermaßen
definiert ist:
const int groesseSp = 1000;
char wortSp[groesseSp];
Über einen Vektor, dessen Komponenten Zeiger auf Zeichenketten enthalten, wird
jeweils das erste Zeichen der im Wortspeicher befindlichen Zeichenketten adressiert:
wortSp
~
u
w e \0 h a n s \0 j
u e
r g e
n \0 p e t
e
r \0
~
zgrZk
[0]
[1]
[2]
[3]
~
~
Abb. 2.2-6: Datenstruktur zum Sortieren von Zeichenketten
Den
Ausbau
dieser
Datenstruktur
übernimmt
die
Funktionsprozedur
„worteLesen()“. Diese Routine wird vom Hauptprogramm aufgerufen, das
zusätzlich noch die Aufrufe für die Ausgabe der Feldkomponenten und den Aufruf an
die Sortierroutine enthält
- Implementierung107
#include <iostream.h>
#include <string.h>
const int groesseSp = 1000;
char wortSp[groesseSp];
void worteLesen(char* z[], int& n)
{
char* zgrwortSp = wortSp;
cout << "\nAnzahl Worte ";
cin >> n;
cout << "\nWorte eingeben: ";
for (int i = 0; i < n; i++)
{
cin >> zgrwortSp; // Einlesen Zeichenkette
z[i] = zgrwortSp; // Zuweisen Zeiger auf Zeichenkette
zgrwortSp +=strlen(zgrwortSp) + 1; //
}
}
107
PR22307.CPP
136
Die Programmiersprache C++
void bubbleSort(char* z[], int n)
{
char* h;
for (int i = 0; i < n - 1; i++)
for (int j = n - 1; i < j; j--)
if (strcmp(z[j-1], z[j]) > 0)
{
h = z[j- 1];
z[j - 1] = z[j]; z[j] = h;
}
}
void ausgabe(char* z[], int n)
{
for (int i = 0; i < n; i++) cout << z[i] << " ";
}
int main()
{
const int anzWorte = 50;
char* zgrZk[anzWorte];
int n;
worteLesen(zgrZk,n);
bubbleSort(zgrZk,n);
ausgabe(zgrZk,n);
}
Dynamisch erzeugte mehrdimensionale Arrays
Mehrdimensionale Felder werden durch Deklaration von Vektoren des Typs array
aufgebaut.
[0]
[1]
[2]
[...]
[n-1]
[0]
...
.....
.....
...
.....
...
.....
[m-1]
.....
Abb. 2.2-7: Dynamische Speicherbelegung für ein zweidimensionales Objekt
Für die dynamische Reservierung des Speicherplatzes wird der Heap, in dem Platz
für dynamische Variablen reserviert und wieder freigegeben werden kann, benutzt.
Die Reservierung von Hauptspeicher auf dem Heap übernimmt
void* malloc(size_t groesse)
137
Die Programmiersprache C++
Bei fehlerfreier Ausführung liefert malloc einen Zeiger, der auf den reservierten
Speicherbereich zeigt, Ist kein Speicherplatz ausreichender Größe zur Verfügung,
dann ist der Rückgabewert Null. Mit
void* calloc(size_t anzElemente, size_t groesse)
wird der Heap verwaltet. Es wird Speicherbereich von anzElemente Bytes
reserviert, die mit Null initialisiert werden. „calloc“ liefert einen Zeiger auf den
reservierten Speicherbereich. Steht kein Speicherbereich ausreichender Größe zur
Verfügung oder anzElemente bzw. groesse haben den Wert Null, dann liefert
calloc Null zurück.
Ein über calloc, malloc reservierter Speicherblock wird mit
void free(void* block)
wieder freigegeben.
Bsp.: Dynamische Speicherbelegung für eine Matrix
// Eingabe von Koeffizientenwerten
void liesmatrix(float** matrix, int zeilen, int spalten)
{
float wert;
for (int i = 0; i < zeilen; i++)
for (int j = 0; j < spalten; j++)
{
cout << "Koeffizientenwert? ";
cin >> wert;
matrix[i][j] = wert;
}
}
// Speicher freigeben
void freispeicher(float** x, int zeilen)
{
unsigned i;
for (i = 0; i < zeilen; i++)
free(x[i]);
// Spalten loeschen
free(x);
// Zeilen loeschen
}
int main()
{
int zeilen, spalten;
float** koeffmatrix; // Koeffizientenmatrix
cout << "\nAnzahl Zeilen / Spalten? ";
cin >> zeilen;
spalten = zeilen;
// 1. Schritt: Setzen der Zeilen von der Koeffizientenmatix
koeffmatrix = (float**) calloc (zeilen, sizeof (float *));
// 2. Schritt: Setzen der Spalten von der Koeffizientenmatrix
for (i = 0; i < zeilen; i++)
koeffmatrix[i] = (float*) calloc (spalten, sizeof (float ));
// Eingabe der Koeffizientenmatrix
cout << "Eingabe der Koeffizientenmatrix" << endl;
liesmatrix(koeffmatrix,zeilen,spalten);
// Freigabe des Speichers
freispeicher(koeffmatrix,zeilen);
}
138
Die Programmiersprache C++
Auch die Operatoren new und delete beschaffen dynamisch Speicherplatz für
Vektoren. Ist der bei new angegebenen Typ ein array, dann gibt operator
new[]() einen Zeiger auf das erste Element des Vektors (array) zurück. Sollten
mehrdimensionale Felder angelegt werden, dann sind alle Dimensionen der
Vektoren anzugeben, z.B
matrixZgr = new int[3][20][12]
Falls ein Vektor mit operator delete[]() gelöscht wird, muß das so beschrieben
werden
delete [] ausdruck
z.B.:
char* zgr;
void funk()
{
zgr = new char[100];
delete [] zgr;
}
// reserviert Platz fuer 100 Zeichen
Bsp.: Dynamische Speicherbelegung für eine Matrix108
// Ein-, Ausgabe von Matrizen
void liesmatrix(float** matrix, int zeilen, int spalten)
{
float wert;
for (int i = 0; i < zeilen; i++)
for (int j = 0; j < spalten; j++)
{
cout << "Koeffizientenwert? "; cin >> wert;
matrix[i][j] = wert;
}
}
int main()
{
float** koeffmatrix; // Koeffizientenmatrix
int zeilen, spalten; unsigned i;
cin >> zeilen; spalten = zeilen;
// 1. Schritt: Setzen der Zeilen von der Koeffizientenmatix
koeffmatrix = new float*[zeilen];
// 2. Schritt: Setzen der Spalten von der Koeffizientenmatrix
for (i = 0; i < zeilen; i++) koeffmatrix[i] = new float[spalten];
// Eingabe der Koeffizientenmatrix
cout << "Eingabe der Koeffizientenmatrix" << endl;
liesmatrix(koeffmatrix,zeilen,spalten);
// Freigabe des Speichers
delete [] koeffmatrix;
}
Zeiger und Vektoren
Zwischen Zeigern und Vektoren besteht eine enge Beziehung. So zeigt bspw. "def"
nach der folgenden Definition
108
PR22310.CPP
139
Die Programmiersprache C++
char *def = "#define";
genau auf die Anfangsadresse der Zeichenkette "#define". "def" ist damit eine
Adreßvariable. Eine Zuweisung
char z;
z = *def
bedeutet: z enthält das Zeichen "#". Mit ++def kann die Adresse, um die Länge von
char (, 8 Bit ,) weitergesetzt werden. Danach wäre in *def "d" enthalten.
Der Name einer Variablen bestimmt die Anfangsadresse des 1. Feldelements.
Unterschiedlich zu Zeigern, kann diesem Vektornamen keine Adresse zugeordnet
werden, da der Vektorname eine Adreßkonstante ist. Ein Zeiger ist dagegen eine
Adreßvariable.
Zeigervektoren zur Behandlung von Argumenten in Programmen
Zeigervektoren spielen bei der Behandlung von Argumenten in C++- Programmen
eine wesentliche Rolle. Ein C++-Programm beginnt seine Ausführungen bekanntlich
mit einem Aufruf der Funktion main(). Diese Funktion ist so deklariert:
int main(int argc, char *argv[], char *arge[])
"argc"109 bestimmt die Anzahl der Argumente in "argv"110. "argv" ist ein
Adressverweis auf einen Vektor der Argumente.
Enthält ein Kommando beim Aufruf 3 Argumente A1, A2, A3, dann hat bzw. ist bzw.
enthält
-
-
"argc" den Wert 4,
"argv[0]" der Name des Kommandos, "argv[1]" ein Adressverweis auf die Zeichenkette der
Argumente A1, "argv[2]", "argv[3]" Adressverweise auf die Zeichenketten der Argumente A2
bzw. A3, "argv[4]" enthält das Zeichen '\0'.
die Variable "arge" Informationen über die Programmumgebung. Zu den Zeichenketten, die
üblicherweise in der Programmungebung enthalten sind, gehören:
-- der Suchpfad, der zur Lokalisierung von Programmen dient (PATH)
-- das Login-Directory (HOME)
-- der Name des Terminals
Diese Daten werden beim Start des Programms übergeben:
Bsp.:
1. Übergabe von Argumenten in der Kommandozeile von Programmen 111
In C++-Programmen können beim Aufruf einer ausführbaren Datei (z.B. a.out
bzw. pr22311.EXE) Argumente in der Kommandozeile übergeben werden (z.B.
a:\\22311.CPP und a:\\pr22311.LST). Diese Argumente werden in
argv[1] bzw. argv[2] gespeichert. Damit stehen sie dem Programmierer zur
Verfügung. Hier soll das 1. Argument auf eine Eingabedatei hinweisen, die
109
argument counter
argument value
111 PR22311.CPP
110
140
Die Programmiersprache C++
eingelesen werden soll. Das 2. Argument verweist auf eine Ausgabedatei, die im
wesentlichen den Inhalt der Eingabedatei aufnehmen soll. Zusätzlich soll jede
Zeile durchnummeriert werden.
[0]
a
:
\
p
r
[1]
a
:
\
p
r
[2]
a
:
\
p
r
2
2
2
2
2
2
1
1
1 1
.
C P
1
.
L S T \0
3
3
3
1
\0
P \0
argv[]
Abb. 2.2-8: Kommandozeilen in einem C++-Programm
#include <fstream.h>
#include <iomanip.h>
int main(int argc,char **argv)
{
int i = 1;
char zeile[250]; // eingabedatei[50], ausgabedatei[50];
if (argc == 1)
{
cout << "\nDateinamen fuer Eingabe/Ausgabe erforderlich";
return(-1);
}
/*
// Alternativ kann mit der Methode get() die Eingabe des Dateinamen
// ueber die Tastatur ermoeglicht werden:
cout << "Name der Eingabedatei: " << flush;
cin.get(eingabedatei,50);
*/
// Erzeugen des Objekts „eingabe“ der Eingabeklasse ifstream
ifstream eingabe(/* eingabedatei */ argv[1],
ios::in|ios::nocreate);
/* 1. Parameter beschreibt das aufzunehmende Feld
2. Paramter beschreibt die maximale Laenge der einzulesenden Zeichen
3. Parameter ist moeglich und gibt das Stopzeichen an. bei dem die
Eingabe auf jedem Fall endet. Fehlt dieser Parameter gilt der
Standardwert '\n'
*/
ofstream ausgabe;
/*
cout << "Name der Ausgabedatei: " << flush;
cin.get(ausgabedatei,50);
*/
ausgabe.open(/* ausgabedatei */ argv[2],ios::out);
if (!ausgabe)
cout << "Kann Datei " << ausgabe << " nicht anlegen\n ";
if (eingabe.good())
{
eingabe.seekg(0L,ios::end);
// Positionieren an das Ende der Datei
// Das 1. Argument bestimmt den relativen Abstand vom zweiten
141
Die Programmiersprache C++
// Argument
cout << "Datei: " << /*eingabedatei*/ argv[1]
<< eingabe.tellg() << " Bytes " << endl;
<< "\t"
/* tellg() ermittelt die Dateizeigerposition in Bytes ab
Dateianfang, d.h. die Dateigroesse, da der Zeiger ans
Ende der Datei gesetzt wird
*/
for (int j = 0; j < 80; j++) cout << "-";
cout << endl;
eingabe.seekg(0L, ios::beg);
// Setzen des Dateizeigers auf den Dateianfang
eingabe.getline(zeile,250);
// funktioniert wie get(), entfernt aber das Stoppzeichen aus dem
// Datenstrom
while(eingabe.good())
// good() erkennt Lesefehler oder das Dateiende und liefert dann
// FALSE
{
ausgabe << "/*" << setw(2) << i++
<< "*/ " << zeile << endl;
// ausgabe << zeile << endl;
eingabe.getline(zeile,250);
}
}
else cout << "Dateifehler oder Datei nicht gefunden"
<< endl;
ausgabe.close();
return(0);
}
Das vorliegende Programm erzeugt eine Ausgabedatei, die folgende Gestalt besitzt:
/* 1*/ #include <fstream.h>
/* 2*/ #include <iomanip.h>
/* 3*/
/* 4*/ int main(int argc,char **argv)
/* 5*/ {
/* 6*/ int i = 1;
/* 7*/ char zeile[250]; // eingabedatei[50], ausgabedatei[50];
/* 8*/ if (argc == 1)
/* 9*/ {
/*10*/
cout << "\nDateinamen fuer Eingabe/Ausgabe erforderlich";
/*11*/
return(-1);
/*12*/ }
..................................................................
..................................................................
Das vorliegende Programm zeigt eine Reihe von Ein- Ausgabeanweisungen zur
Bearbeitung von Dateien der iostream-Klasse112. Für grundlegende Dateioperationen
gibt es in C++ drei Klassen:
-
Die ofstream-Klasse für die Ausgabe in eine Datei
Die ifstream-Klasse für das Lesen aus einer Datei
Die fstream-Klasse für die Ein- und Ausgabe
2. Informationen über die Programmumgebung
#include <iostream.h>
main(int argc, char *argv[], char *arge[])
{
while (*arge)
112
vgl. 3.4
142
Die Programmiersprache C++
cout << *arge++ << endl;
}
Schwächen des Feldkonzepts sind:
-
keine Laufzeitüberwachung für Indexausdrücke
keine Vereinbarung von beliebigen Indexbereichen
keine Reduzierung einmal angelegter Felder
keine bequemes Kopieren gleichartiger Felder
2.2.3.2 Der C++-Standardtyp vector
Tabellen mit einer Spalte werden in C++ durch eine „Vektor“ genannte Konstruktion
(vordefinierte Klasse) gebildet. Ein „Vektor“ ist eine Tabelle von Elementen
desselben Datentyps, z.B. mit ganzen Zahlen oder mit double-Zahlen. In C++ ist
ein Vektor eine vordefinierte Klasse. Die Anweisung
vector<int> v(10)
stellt einen Vektor v mit 10 Elementen vom Typ int bereit. Feldelemente sind in
C/C++ von 0 bis (Anzahl der Elemente – 1) durchnumeriert (hier von 0 bis 9). Es gibt
keine Überprüfung der Bereichsüber- oder –unterschreitung.
Die Klasse für Vektoren113 stellt einige Dienstleistungen zur Verfügung, z.B.:
-
-
size() ermittelt die Größe des Vektors, im vorliegenden Fall zeigt „cout << v.size() <<
endl;“ die tatsächliche Größe des Vektors an.
capacity() bestimmt die maximal mögliche Anzahl der Vektorelemente, die in dem Vektor
gespeichert werden können.
Indexoperator [] realisiert den Zugriff auf ein spezielles Element des Vektors. „v[0]“ zeigt über
„cout << v[0];“ das erste Element des Vektors an. Man kann auf die eckigen Klammern
verzichten und einen Vektor über at nach einem Wert an einer Position fragen, z.B. cout <<
v.at(0) << endl;. Diese Art des Zugriffs wird geprüft.“cout << v.at(20) << endl“ führt
zum Programmabbruch mit Fehlermeldung.
push_back() Ein Vektor der C++-Standardbibliothek hat den Vorteil, daß er bei Bedarf Elemente
hinten anhängt und dabei seine Größe ändert. Über push_back(wert) wird der anzuhängende
„wert“ übergeben und ohne weiteres Zutun des Programmierers gespeichert.
Bsp.: Einfache Sortierverfahren
#include <iostream>
#include <vector>
// Standard-Vektor
using namespace std;
// Funktionsprototypen
void eingabe(vector<int>&, int&);
void ausgabe(vector<int>, int);
void bsort(vector<int>&, int);
void auswahlsortieren(vector<int>&, int);
void einfuegesortieren(vector<int>&, int);
void tauschen(vector<int>&, int, int);
int main()
113
vgl. 5.2.8
143
//
//
//
//
//
//
Eingabe
Ausgabe
Bubble-Sort
Sortieren durch Auswaehlen
Sortieren durch Einfuegen
Tauschen
Die Programmiersprache C++
{
int n;
// int x[20];
// vector<int> x(20);
vector<int> x; // anfaengliche Groesse ist 0
eingabe(x,n);
cout << "Ausgabe unsortiert:\n";
ausgabe(x,n);
cout << "Ausgabe
sortiert:\n";
bsort(x,n);
// einfuegesortieren(x,n);
// auswahlsortieren(x,n);
ausgabe(x,n);
return 0;
// kann weggelassen werden
}
// Eingabe
void eingabe(vector<int>& x, int& n)
{
int wert;
// cout << "Anzahl Elelemente?\n";
// cin
>> n;
cout << "Eingabe von Werten\n";
do
{
cout << "Wert (0 = Ende der Eingabe):";
cin >> wert;
if (wert != 0)
x.push_back(wert);
// Wert anhaengen
} while (wert != 0);
n = x.size();
}
// Ausgabe
void ausgabe(vector<int> x, int n)
{
const int zeilenLaenge = 12; // Anzahl der Elemente je Zeile
cout << "(" << n << ") <";
for (int i = 0; i < n; ++i)
{
if (i % zeilenLaenge == 0 && i) // ;
cout << "\n\t";
cout << x[i];
// Trennen durch , Ausnahme letztes Element
if (i % zeilenLaenge != zeilenLaenge - 1 && i != n - 1)
cout << ", ";
}
cout << ">\n";
}
// Sortieren
void bsort(vector<int>& x, int n)
{
// Sortieren durch Austauschen
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
// if ((*vrgl) (x[i], x[j]) < 0)
if (x[i] > x[j]) tauschen(x,i,j);
}
void auswahlsortieren(vector<int>& x, int n)
{
int min;
for (int i = 0; i < n - 1; i++)
{
min = i;
for (int j = i + 1; j < n; j++)
if (x[j] < x[min]) min = j;
144
Die Programmiersprache C++
tauschen(x,min,i);
}
}
void einfuegesortieren(vector<int>& x, int n)
{
int schl, j;
for (int i = 1; i < n; i++)
{
schl = x[i];
j
= i - 1;
while ((schl < x[j]) && (j >= 0))
{
x[j + 1] = x[j];
j--;
}
x[j + 1] = schl;
}
}
void tauschen(/* int* x,*/ vector<int>& x, int i, int j)
{
int zwischen = x[i];
x[i] = x[j];
x[j] = zwischen;
}
2.2.3.3 Die C++-Stringklasse
Die C++-Stringklasse (Header: <string>) kann in den meisten Fällen C-Strings
ersetzen. Der in der Standardbibliothek definierte Typ basic_string ist ein
Template. Der Typ string ist eine Spezialisierung von basic_string für den
Datentyp char: typedef basic_string<char> string;.
Strings existieren nicht nur basierend auf dem Datentyp char, sondern auch noch
einmal basierend auf dem Datentyp wchar_t. Dieser Datentyp dient zur
Speicherung eines 16-Bit-Zeichens, d.h. eines Zeichens im Unicode-Zeichensatz.
Strings, die auf wchar_t basieren, gehören zur Klasse wstring, während Strings,
die auf char basieren, zur Klasse string gehören. Es handelt sich in beiden Fällen
nur um verschiedene Instantiierungen derselben Basisklasse (basic_string). Alle
Members sind gleich.
Strings sind in C++ einfach zu handhaben und robust. Es gibt bspw. hier die üblichen
Operatoren:
<<
+
==
<=
>=
>>
+=
!=
<
>
Ein- und Ausgabe
Verketten, Anhängen
Prüfung auf Gleichheit, Ungleichheit
Alphabetische Vergleiche auf kleiner bzw. kleiner, gleich
Alphabetische Vergleiche auf größer bzw. größer, gleich
Erzeugen von String-Objekten.
Beim Anlegen von String-Objekten braucht man keine maximale Länge anzugeben,
weil Strings automatisch mitwachsen, wenn sich ihr Inhalt ändert. Beim Initialisieren
können C-Zeichenketten und andere Strings verwendet werden. Sogar das
145
Die Programmiersprache C++
Ausschneiden von Teilzeichenketten geht, ohne daß man dazu ein spezielle
Methode braucht.
Arbeiten mit String-Objekten
Zuweisen von String-Objekten: Strings können ohne Probleme zugewiesen werden.
Lästige Aufrufe, z.B. strcpy, sind unnötig. Für das Zuweisen von Teilzeichenketten
gibt es die Methode assign():
string zk1("Dies ist eine lange Zeichenkette");
string zk2;
zk2.assign(zk1, 14, 4); // zugewiesen wird "lang"
Längenbestimmung von String-Objekten: Sie erfolgt mit der Methode length()
Zugriff auf einzelne Zeichen von String-Objekten: Man kann hier dieselbe
Schreibweise wie bei den C-Zeichenketten verwenden, weil der []-Operator
überladen wurde. Gleichbedeutend zu dieser Art des Zugriffs ist die Anwendung der
Methode at(). Im Unterschied zu dem Operator [] enthält at() eine
Bereichsüberprüfung, die ggf. eine Ausnahme erzeugt.
Einfügen und Löschen von Zeichen in String-Objekten: Die Methode insert()
erlaubt das Einfügen von Zeichen und Zeichenketten an beliebiger Stelle im String.
Ein Überlaufen kann nicht passieren , da der String nötigenfalls vergrößert wird. Zum
Anhängen an das Ende verwendet man einfacher den Operator +=.
Suchen und Ersetzen von String-Objekten: Zum Suchen gibt es die Methoden
find() zum Suchen vom Beginn der Zeichenkette und rfind(), die von rechts
sucht. Zum Ersetzen gibt es die Methode replace(). Hier werden die Startposition,
ab der ersetzt werden soll, die Anzahl der Zeichen, die ersetzt werden soll, und die
Zeichenkette, die stattdessen eingesetzt werden soll, angegeben.
Zeichen löschen. iterator erase(iterator p) löscht ein Zeichen an Stelle p.
Zurückgegeben wird die Position direkt vorher, sofern sie existiert, andernfalls
end(). iterator erase(iterator p, iterator q) löscht Zeichen im
Bereich p bis ausschließlich q. string& erase(size_type pos =
0,size_type n = npos) löscht alle Zeichen ab der Stelle pos, aber nicht mehr
als npos.
Einlesen von String-Objekten: Strings können mit dem üblichen <<-Operator
eingelesen werden. Es wird immer bis zu einem Whitespace gelesen. Möchte man
mehr als nur ein Wort – üblicherweise eine Zeile – einlesen, dann verwendet man
die Funktion :istream& getline(istream&, string&, char=‘\n‘);
Speicherplatzreservierung für String-Objekte. Damit ein String nicht ständig
reallokiert werden muß, weil er öfters mal wächst, kann man für ihn direkt eine
bestimmte
Größe
reservieren.
Das
geht
mit
der
Methode
reserve(string::size_type).
146
Die Programmiersprache C++
Weitere Schnittstellenfunktionen der Klasse string
Konstruktoren
string()
// Standarkonstruktor, erzeugt einen leeren String.
string(const string& s, size_type pos = 0, size_type n = npos)
// Der Kopierkonstruktor erzeugt einen String, wobei s ab Postion pos bis zum Ende kopiert wird.
// Dabei gilt die Einschränkung, daß maximal n Zeichen kopiert werden. „string::npos“ ist eine
// –1, konvertiert zum Typ size_type, in der Regel die größtmögliche unsigned Zahl.
string(const char* s, size_type n)
// n Zeichen werden aus dem bei s beginnenden Array kopiert
string(const char* s)
// erzeugt einen String aus dem C-String s
string(size_type n, char z)
// erzeugt einen String mit n Kopien von z.
Destruktor
~string()
// Anfang und Ende eines Strings
const_iterator begin() const und iterator begin()
// geben den Anfang des Strings zurück
const_iterator end() const und iterator end()
// geben die Position nach dem letzten Zeichen zurück Methoden
size_type size() const
// gibt die aktuelle Größe des Strings zurück (Anzahl der Zeichen)
void clear()
// löscht den Inhalt des String
bool empty() const
// gibt size() == 0 bzw. begin() == end zurück
string& append(const string& s)
string& append(const char* s)
string& append(char z)
string& operator+=(const string& s)
string& operator+=(const char* s)
string& operator+=(char z)
// verlängern den String um den C-String bzw. String s bzw. das Zeichen z
string append(const string& s, size_type pos, size_type n)
// Von der Position pos des String s bis zum Ende wird alles an den String angehängt, aber nicht
// mehr als n Zeichen.
string& assign(const string& s)
string& assign(const char* s)
string& assign(char c)
string& operator=(const string& s)
string& operator=(const char* s)
string& operator=(char z)
// weisen dem String den C-String oder String s bzw. das zeichen z zu.
string substr(size_type pos = 0, size_type n = npos) const
// gibt den Substring zurück, der ab pos beginnt. Die Anzahl der Zeichen im Substring wird durch
// das Ende des String bestimmt, kann aber nicht größer als n werden.
size_type find(const string& s, size_type pos = 0) const
// gibt die Position zurück, an der der Substring s gefunden wird, anderenfalls wird string::npos
// zurückgegeben.
int compare(const string& s) const
// vergleicht zeichenweise die Strings *this und s. Es wird 0 zurückgegeben, wenn keinerlei
// Unterschied festgestellt wird. Falls das erste unterschiedliche Zeichen von *this kleiner als das
// entsprechende in s ist, wird eine negative Zahl zurückgegeben, andernfalls eine positive Zahl.
// Falls bei unterschiedlicher Länge bis zum Ende eines der Strings keine verschiedenen Zeichen
// gefunden werden, wird eine negative Zahl zurückgegeben, falls size() < s.size() ist,
// andernfalls eine positive Zahl.
int compare(size_type pos,size_type n,const string& s) const
// gibt string(*this,pos,n).compare(s) zurück. Es werden nur die Zeichen ab Position pos
147
Die Programmiersprache C++
// in *this berücksichtigt, aber maximal n.
Operatoren:
string operator+(const string&, const string&)
string operator+(const string&, const char*)
string operator+(const char*, const string&)
// Diese Operatoren verketten zwei Strings und geben das Ergebnis zurück.
string operator+(const string&,char)
string operator+(char,const string&)
// Diese Operatoren verketten einen String mit einem Zeichen.
bool operator==(x,y)
bool operator!=(x,y)
bool operator<=(x,y)
bool operator>=(x,y)
bool operator<(x,y)
bool operator>(x,y)
// sind relationale Operatoren zum Vergleichen von Strings. x und y stehen hier jeweils für einen
// Type string& oder const char*. Es sind drei Kombinationen für x und y möglich:
//
const string&, const string&
//
const char*, const string&
//
const string&, const char*
istream& operator>>(istream&,string&)
// Dieser Operator erlaubt das Einlesen von Strings auf bequeme Weise. Die üblichen
// Eigenschaften des „>>“-Operators werden beibehalten
ostream operator<<(ostream&,string&)
// Ausgabeopeartor für Strings
istream& getLine(istream& is, string& s, char ende = '\n')
// liest Zeichen für Zeichen aus der Eingabe is in den String s bis das Zeichen „ende“ eingelesen
// wird.
Bsp.: Typische String-Operationen114.
#include <iostream.h>
#include <string>
using namespace std;
int main()
{
// Anlegen eines String-Objekts
string einString("Hallo");
// Ausgabe eines String
cout << einString << endl;
// Zeichenweise Ausgabe eines String mit ungeprueftem Zugriff
for (size_t i = 0; i < einString.size(); i++)
cout << einString[i];
cout << endl;
// Zeichenweise Ausgabe eines String mit Indexpruefung
for (size_t i = 0; i < einString.size(); i++)
cout << einString.at(i);
cout << endl;
/* Der Versuch einString.at(i) mit i >= einString.size()
abzufragen, fuehrt zum Programmabbruch und Fehlermeldung
*/
// Kopie eines String erzeugen
string eineStringkopie(einString);
cout << eineStringkopie << endl;
// Kopie durch Zuweisung
string zuweisungsKopie("Kopie durch Zuweisung");
eineStringkopie = zuweisungsKopie;
cout << eineStringkopie << endl;
// Zuweisung einer Zeichenkette
114
PR22230.CPP
148
Die Programmiersprache C++
eineStringkopie = "Informatik";
cout << eineStringkopie << endl;
// Zuweisen eines einzelnen Zeichens
einString = 'X';
cout << einString << endl;
// Strings mit + verketten
einString = "Hallo " + eineStringkopie + '!';
cout << einString << endl;
}
2.2.4 Strukturen
Eine Struktur ist ein Datenaggregat115, in dem heterogene Teilobjekte zu einer
Einheit zusammengefaßt werden. In C++ erfolgt dies durch eine "struct"Spezifikation.
struct studentenSatz
{
char name[20];
char vorname[20];
char strasse[20];
unsigned short hausnr;
char plz[6];
char ort[20];
} student;
Diese Definition vereinbart:
-
eine Strukturvariable "student"
einen Strukturtyp "studentenSatz"
Die Typdefinition erlaubt es, weitere Objekte von diesem Strukturtyp zu definieren,
z.B.:
studentenSatz s[100];
// äquivalent mit struct studentenSatz s[100]
Der Zugriff auf die Komponenten (member) erfolgt über den Punktoperator:
void ausgabestudent(studentenSatz&
{
cout << "Name: "
<< s.name
cout << "Vorname: " << s.vorname
cout << "Strasse: " << s.strasse
cout << "Haus-Nr.: " << s.hausnr
cout << "PLZ: "
<< s.hausnr
cout << "Ort: "
<< s.hausnr
}
s)
<<
<<
<<
<<
<<
<<
"\n";
"\n";
"\n";
"\n";
"\n";
"\n";
Strukturen können auch geschachtelt sein, z.B.:
struct datum {int tag, monat, jahr};
struct studentenSatz
{
char name[20];
char vorname[20];
char strasse[20];
115
Nicht objektorientierter Aspekt des struct-Datentyps
149
Die Programmiersprache C++
unsigned short hausnr;
char plz[6];
char ort[20];
datum geburtstag;
} student;
void ausgabestudent(student_satz& s)
{
.....................
cout << "Geburtsdatum: "<< s.geburtstag.tag << "."
<< s.geburtstag.monat << "." << s.geburtstag.jahr << "\n";
}
Wie Felder können Strukturen auch initialisiert sein, z.B.:
datum geburtstagstudent = {10,4,1954};
Im Gegensatz zu Feldern sind Zuweisungen erlaubt:
studentenSatz a, b;
// "a" wird mit Werten belegt
........
b = a;
Der Strukturname ist sofort nach Einführung gültiger Typname, z.B.:
struct listenknoten
{
infotype info;
listenknoten* nachf;
}
C++ stellt zur Übergabe von Zeigern auf Strukturen einen speziellen StrukturOperator (->) zur Verfügung.
Bsp.:
1. Tauschen zweier Werte in einer Struktur
#include <iostream.h>
struct koord
{
double xk;
double yk;
};
void tauschen(struct koord* werte)
{
double h;
h = werte->xk;
werte->xk = werte->yk;
werte->yk = h;
}
void main()
{
struct koord position = {13.11,19.40};
cout << "\nVorher: " << position.xk << " " << position.yk;
tauschen(&position);
cout << "\nNachher: " << position.xk << " "
<< position.yk;
}
150
Die Programmiersprache C++
2. Sortieren von Datensätzen im Arbeitsspeicher116
Eingabe von Datensätzen und Einlesen der Datensätze in ein Arbeitsspeicherfeld
Sortieren der Datensätze
Die Zeiger in den Komponenten des Arbeitsspeicherfelds werden zu diesem Zweck
umgestellt. Nach dem Sortieren sind die Zeiger in den Komponeneten des
Arbeitsspeicherfelds so bestimmt, daß die Namen in den Datensätzen (über aufsteigende
Subskripte ermittelt) aufsteigend sortiert sind
Ausgabe der sortierten Datensätze auf das Standardausgabegerät
Ausgabe der sortierten Datensätze in eine Textdatei
Einlesen der sortierten Datensätze aus der Textdatei und Ausgabe auf das Standardausgabegerät
-
-
eintrag
name
vorname
Abb. 2.2-9: Datenstruktur zur Adressenverwaltung
#include <iostream.h>
#include <fstream.h>
#include <string.h>
struct eintrag
{
char name[20];
char vorname[15];
};
void main()
{
char* dateiName = "a:ausgD.txt";
int belegung = 100;
int n;
// Anzahl der aktuellen Saetze
eintrag** elemente;
elemente = new eintrag*[belegung];
for (int i = 0; i < belegung; i++)
elemente[i] = new eintrag;
short menue(void), wahl;
void aufnehmenSaetze(eintrag**, int&, char*);
void ausgebenSaetze(eintrag**, int);
void sortierenSaetze(eintrag**, int);
void ausgebenDatei(eintrag**, int, char*);
void eingebenDatei(char*);
cout << "\n\t A D R E S S E N V E R W A L T U N G";
cout << "\n\t -----------------------------------";
do
{
wahl = menue();
switch(wahl)
116
PR22403.CPP
151
Die Programmiersprache C++
{
case 1: aufnehmenSaetze(elemente,n,dateiName);
break;
case 2: sortierenSaetze(elemente,n);
break;
case 3: ausgebenSaetze(elemente,n);
break;
case 4: ausgebenDatei(elemente,n,dateiName);
break;
case 5: eingebenDatei(dateiName);
break;
case 6: break;
}
} while (wahl != 6);
}
short menue(void)
{
short izch;
cout << "\n\tBitte eingeben: ";
cout << "\n\t1: Eingabe von Datensaetzen ";
cout << "\n\t2: Sortieren von Datensaetzen ";
cout << "\n\t3: Ausgabe von Datensaetzen ";
cout << "\n\t4: Speichern von Datensaetzen ";
cout << "\n\t5: Einlesen gespeicherter Datensaetze ";
cout << "\n\t6: Beendigung des Programms ";
cout << "\n\tWahl: ";
do
{
cin >> izch;
} while ((izch < 1) || (izch > 6));
return izch;
}
void aufnehmenSaetze(eintrag** elemente, int& n,
char* dateiName)
{
eintrag* zelem;
int zaehler = 0;
/*
ifstream eingabe(dateiName, ios::in);
if (eingabe.good())
{
while (!eingabe.eof())
{
zelem = new eintrag;
if ((zelem = new eintrag) == 0)
{
cerr << "\n\t Nicht genuegend Speicherplatz vorhanden";
break;
}
eingabe >> zelem->name;
// cout << zelem->name;
eingabe >> zelem->vorname;
// cout << zelem->vorname;
elemente[zaehler++] = zelem;
}
eingabe.close();
}
*/
cout << "\n\tAnzahl der einzugebenden Saetze: ";
cin >> n;
cout << "\tEingabe von " << n << " Datensaetzen\n";
for (int i = 0; i < n; i++)
{
zelem = new eintrag;
cout << "\tName: ";
cin >> zelem->name;
152
Die Programmiersprache C++
cout << "\tVorname: "; cin >> zelem->vorname;
elemente[i] = zelem;
}
n += zaehler;
}
void ausgebenSaetze(eintrag** elemente, int n)
{
for (int i = 0; i < n; i++)
{
cout << '\t' << elemente[i]->name << ", "
<< elemente[i]->vorname << endl;
}
}
void sortierenSaetze(eintrag** elemente, int n)
{
eintrag* h;
for (int i = 0; i < n - 1; i++)
for (int j = n - 1; i < j; j--)
if (strcmp(elemente[j-1]->name, elemente[j]->name) > 0)
{
h = elemente[j-1];
elemente[j-1] = elemente[j];
elemente[j] = h;
}
}
void ausgebenDatei(eintrag** elemente, int n, char* dateiName)
{
ofstream ausgabe(dateiName,ios::out);
for (int i = 0; i < n; i++)
{
ausgabe << elemente[i]->name << '\n';
ausgabe << elemente[i]->vorname << '\n';
}
ausgabe.close();
}
void eingebenDatei(char* dateiName)
{
struct eintrag ausgabeSatz;
ifstream eingabe(dateiName, ios::in);
while (!eingabe.eof())
{
eingabe >> ausgabeSatz.name;
cout << "\n\t" << ausgabeSatz.name << ", ";
eingabe >> ausgabeSatz.vorname;
cout << ausgabeSatz.vorname;
}
eingabe.close();
}
Die Dateien, mit denen bisher gerbeitet wurde, waren Textdateien. Diese Dateien
sind aus Datenzeilen aufgebaut, die mit den Zeichen '\n' abschließen. Beim Lesen
wird dieses Zeichen in die Folge CR+LF (0xD+0xA) umgewandelt. Beim Speichern
wird daraus wieder der Zeilenvorschub '\n'.
Komplizierte Datenstrukturen werden binär (ios::binary) gespeichert. Im
vorliegenden Fall ist dann die Ausgabe bzw. Eingabe in bzw. aus einer Datei so
möglich:
void ausgebenDatei(eintrag** elemente, int n, char* dateiName)
{
struct eintrag ausgabeSatz;
ofstream ausgabe(dateiName,ios::out | ios::binary);
153
Die Programmiersprache C++
for (int i = 0; i < n; i++)
{
ausgabeSatz = *elemente[i];
ausgabe.write((char*) &ausgabeSatz,sizeof ausgabeSatz);
/* Die Methode write() benötigt einen Zeiger auf den
Speicherbereich, der die Daten enthält und eine
Angabe über die Datenfgroesse. Hier werden Adresse
und Groesse des Objekts angegeben. Der cast sorgt
dafuer, dass ein korrekter Pointertyp beim
Kompilieren erkannt wird */
}
ausgabe.close();
}
void eingebenDatei(char* dateiName)
{
struct eintrag eingabeSatz;
ifstream eingabe(dateiName,ios::in | ios :: binary);
if (eingabe.good())
cout << "\n\tEingabedatei erfolgreich eroeffnet" << endl;
// eingabe.setmode(filebuf::binary);
eingabe.read((char*) &eingabeSatz,sizeof eingabeSatz);
// Die Methode read() liest die Daten in den Speicherbereich ein
while (eingabe.good())
{
cout << "\n\t" << eingabeSatz.name << ", "
<< eingabeSatz.vorname;
eingabe.read((char*) &eingabeSatz,sizeof eingabeSatz);
}
eingabe.close();
}
2.2.5 Variantenstruktur
Hier werden die Komponenten nicht hintereinander, sondern quasi übereinander
angelegt. Zu einem bestimmten Zeitpunkt kann nur eine Komponente sinnvolle
Werte enthalten, z.B.:
union int_bzw_float {
int i;
float f;
} v;
Es kann nur auf "i" oder "f" zugegriffen werden, z.B.:
v.i = 17;
cout << "i = " << i << "\n";
v.f = 3.14;
cout << "f = " << v.f << "\n";
cout << "i = " << v.i << "\n"; //Ausgabe: Mist
Alle Komponenten beginnen an der gleichen Stelle. Die Länge einer union ist gleich
der Länge der längsten Komponente.
154
Die Programmiersprache C++
3. Benutzerdefinierte Datentypen: Klassen
3.1 Konzepte für benutzerdefinierte Datentypen
Ziel des C++-Konzepts Klasse (definiert mit struct, union, class) ist: Bereitstellung
eines Werkzeugs für die Erzeugung neuer Datentyypen (, die so bequem wie
eingebaute Typen eingesetzt werden können). Zu einer Klasse gehören:
Datenemente und Verarbeitungselemente (d.h. Funktionen). Daten und (Element-)
Funktionen sind in der Klasse gekapselt117 . Bestandteile einer Klasse können dem
allgemeinen Zugriff entzogen sein (information hiding). Der Programmentwickler
bestimmt die Sichtbarkeit jedes einzelnen Elements. Einer Klasse ist ein Name (TypBezeichner) zugeordnet. Dieser Typ-Bezeichner kann zur Deklaration von Instanzen
oder Objekten des Klassentyps verwendet werden.
3.1.1 Formale Definitionsmöglichkeiten
Definition einer Klasse
Sie besteht aus 2 Teilen
1. dem Kopf, der neben dem Schlüsselwort class (bzw. struct, union) den
Bezeichner der Klasse enthält
2. den Rumpf, der umschlossen von geschweiften Klammern, abgeschlossen durch
ein Semikolon, die Mitglieder (member, Komponenten, Elemente) der Klasse
enthält.
Der Zugriff auf Elemente von Klassen wird durch 3 Schlüsselworte gesteuert:
private: Auf Elemente, die nach diesem Schlüsselwort stehen, können nur
Elementfunktionen zugreifen, die innerhalb derselben Klasse definiert sind.
"class"-Komponenten sind standardmäßig "private".
protected: Auf Elemente, die nach diesem Schlüsselwort stehen, können nur
Elementfunktionen zugreifen, die in derselben Klasse stehen und
Elementfunktionen, die von derselben Klasse abgeleitet sind
public: Auf Elemente, die nach diesem Schlüsselwort stehen, können alle
Funktionen in demselben Gültigkeitsbereich, den die Klassendefinition hat,
zugreifen.
Der Geltungsbereich von Namen der Klassenkomponenten ist klassenlokal, d.h. die
Namen können innerhalb von Elementfunktionen und im folgenden Zusammenhang
benutzt werden:
klassenobjekt.komponentenname
117
Eine Klasse ist vereinfacht ausgedückt eine Struktur (struct), in der nicht nur Daten abgelegt sind, sondern
auch gleichzeitig die Funktionen, die auf diese Daten zugreifen
155
Die Programmiersprache C++
zeiger_auf_klassenobjekt->komponentenname
klassenname::komponentenname
-
-
Der Punktoperator „.“ wird benutzt, falls der Programmierer Zugriff auf ein
Datenelement oder eine Elementfunktion eines speziellen Klassenobjekts
wünscht.
Der Pfeil-Operator „->“ wird benutzt, falls der Programmierer Zugriff auf ein
spezielles Klassenobjekt über einen Zeiger wünscht
Klassen besitzen einen öffentlichen (public) und einen versteckten (private) Bereich.
Versteckte Elemente (protected) sind nur den in der Klasse aufgeführten
Funktionen zugänglich. Programmteile außerhalb der Klasse dürfen nur auf den mit
public als öffentlich deklarierten Teil einer Klasse zugreifen.
Eine mit struct definierte Klasse ist eine Klasse, in der alle Elemente gemäß
Voreinstellung public sind
Die Elemente einer mit union definierten Klasse sind public. Dies kann nicht
geändert werden.
Die Elemente einer mit class definierten Klasse sind gemäß Voreinstellung private.
Die Zugriffsebenen können verändert werden.
Datenelemente und Elementfunktionen
Datenelemente einer Klasse werden genau wie die Elemente einer Struktur
angegeben. Eine Initialisierung der Datenelemente ist aber nicht erlaubt. Deshalb
dürfen Datenelemente auch nicht mit const deklariert sein.
Funktionen einer Klasse sind innerhalb der Klassendefinition deklariert. Wird die
Funktion auch innerhalb der Klassendefinition definiert, dann ist diese Funktion
inline. Elementfunktionen von Klassen unterscheiden sich von den gewöhnlichen
Funktionen:
-
Der Gültigkeitsbereich einer Klassenfunktion ist auf die Klasse beschränkt (class scope).
Gewöhnliche Funktionen gelten in der ganzen Datei, in der sie definiert sind
Eine Klassendefinition kann automatisch auf alle Datenelemente der Klasse zugreifen.
Gewöhnliche Funktionen können nur auf die als "public" definierten Elemente einer Klasse
zugreifen.
Die Funktionen einer Klasse haben die Aufgabe, alle Manipulationen an den Daten
dieser Klasse vorzunehmen.
Der „this“-Zeiger
Jeder (Element-) Funktion wird ein Zeiger auf das Element, für das die Funktion
aufgerufen wurde, als zusätzlicher (verborgener) Parameter übergeben. Dieser
Zeiger heißt this, ist automatisch vom Compiler) deklariert und mit der Adresse der
jeweiligen Instanz (zum Zeitpunkt des Aufrufs der Elementfunktion) besetzt. "this" ist
als *const deklariert, sein Wert kann nicht verändert werden. Der Wert des Objekts
(*this), auf den this zeigt, kann allerdings verändert werden.
156
Die Programmiersprache C++
3.1.2 Ein einführendes Beispiel: Stapelverarbeitung
Ein Stapel ist eine Datenstruktur zur Aufnahme bzw. Abgabe von Daten (hier: ganze
Zahlen). Die Aufnahme der Daten (Operation: push) bzw. die Abgabe der Daten (
(Operation: pop) kann nur von oben (Topelement, Zugriff über die Funktion top() )
erfolgen. Der Stapel soll Elememte118 vom Typ "int" aufnehmen.
Schnittstellenbeschreibung
#include <iostream.h>
const int stapelgroesse = 100;
class intStapel
// ein neuer Typname wird eingefuehrt
{
private:
// exklusiv für Methoden der Klasse
int inhalt[stapelgroesse]; // Array-Implementation
int nachf;
// Index des nachfolgenden Elements
public:
// steht allen Benutzern zur Verfügung
intStapel& push(int);
intStapel& pop();
int top();
int stapeltiefe();
// Stapeltiefe
int istleer();
};
Schnittstellenfunktionen (Elemenent-)
Es gibt zwei Möglichkeiten zur Angabe von Elementfunktionen in Klassen:
-
Definition der Funktion innerhalb von Klassen
Deklaration der Funktion innerhalb, Definition der Funktion außerhalb der Klasse.
Mit dem Scope-Operator :: wird beider Defintion der Funktion außerhalb der Klasse
dem Compiler mitgeteilt, wohin die Funktion gehört.
Beim Aufruf von Elementfunktionen muß der Name des Zielobjekts angegeben
werden: klassenobjektname.elementfunktionen(parameterliste)
1. Deklaration der Funktion innerhalb, Definition der Funktion außerhalb der Klasse.
// Elementfunktionen
intStapel& intStapel::push(int wert)
/* push() soll einen "intStapel" zurueckgeben, aus Effizienzgruenden
ist eine Referenz angebracht. push() steht ausserhalb der Klassendefinition, muss daher mit dem Klassennamen ueber den Operator ::
verbunden werden: intStapel::push() bedeutet: Die Methode push()
der Klassen "intStapel" */
{
if (nachf < stapelgroesse)
inhalt[nachf++] = wert;
// unmittelbarer Zugriff
else
// primitive Fehlerbehandlung
cerr << "Fehler in push() : Stapel ist voll! \n";
return *this;
118
pr31201.CPP
157
Die Programmiersprache C++
/* Das Schluesselwort this entspricht einem in Komponentenfunktionen
immer vorhandenen Zeiger auf die Instanz, die Ausdruecke nachf und
inhalt sind daher zu this->inhalt und this->nachf aequivalent.
*this repraesentiert den vorliegenden Stapel, die Referenz
darauf ermittelt der Uebersetzer automatisch */
}
intStapel& intStapel::pop() { nachf--; return *this; }
int intStapel::top() { return inhalt[nachf - 1]; }
int intStapel::stapeltiefe() { return nachf; }
int intStapel::istleer() { return nachf == 0; }
// Ein kleines Testprogramm
main()
{
intStapel s; // Instanz
s.push(7); s.push(11); s.push(21);
while ( !s.istleer() )
{
cout << s.top() << " "; s.pop();
}
return 0;
}
2. Definition der Funktion innerhalb von Klassen
Elementfunktionen können auch innerhalb der Klassendefinition definiert werden,
womit sie automatisch als "inline" vereinbart gelten
#include <iostream.h>
const int stapelgroesse = 100;
class intStapel
{
private:
int inhalt[stapelgroesse];
int nachf;
public:
intStapel() : nachf(0)
intStapel(int e) : nachf(0)
intStapel& push(int w)
intStapel& pop()
int top()
int stapeltiefe()
int istleer()
};
// konstante Klassenvariable?
{ }
{ push(e); }
{ inhalt[nachf++] = w; return *this; }
{ nachf--; return *this; }
{ return inhalt[nachf - 1]; }
{ return nachf; }
{return nachf == 0; }
158
Die Programmiersprache C++
3.1.2 Konstruktoren, Klassenvariable und Destruktoren
Konstruktoren
Das einführende Beispiel ist falsch. Es fehlt die Initialisierung. Die naive Lösung zur
Initialisierung, z.B.
intStapel s; s.nachf = 0;
scheitert (auf "nachf" darf als private Komponente außerhalb einer Stapel-Methode
nicht zugegriffen werden).
Konstruktoren dienen zur Initialisierung von Klassen-Objekten, haben keinen
Ergebnistyp und sind namensgleich mit ihrer Klasse, z.B.:
intStapel::intStapel() {nachf = 0; }
Über Argumente kann ein Konstruktor verfügen, z.B.:
intStapel::intStapel(int erstesElement)
{
nachf = 0;
push(erstesElement);
/* Bei der Defimition des Stapel wird zugleich das erste
Element gestapelt */
}
Aktiviert wird der Konstruktor über
intStapel s(7);
bzw.
intStapel s = 7;
// Initialisierung, keine Zuweisung
Konstruktoren können überladen werden und Vorgabewerte definieren. Falls
mehrere Konstruktoren vorliegen, bestimmt der Compiler (anhand der
Parameterlisten), welcher Konstruktor zu verwenden ist. Konstruktoren
unterscheiden sich hinsichtlich Anzahl und/oder Typ der Parameter.
Es ist nicht möglich, einen zweiten Konstruktor mit Hilfe eines ersten Konstruktors zu
intialisieren:
intStapel::intStapel(int erstesElement)
{
intStapel(); push(erstesElement); // Fehler
}
Für die Hauptaufgabe eines Konstruktors (Initialisierung einer Instanstanzvariablen)
gibt es eine besondere Syntax, z.B.:
intStapel :: intStapel() : nachf(0)
{ }
intStapel :: intStapel(int e) : nachf(0) { push(e); }
Werden mehrere Komponenten auf diese Weise initialisiert, entspricht die
Reihenfolge der Initialisierung der Reihenfolge der Deklaration, z.B.:
#include <iostream.h>
int f(int i) { cout << i << " "; return i; }
159
Die Programmiersprache C++
class X
{
int a, b, c;
public:
X() : a(f(1)), c(f(2)), b(f(3)) { }
};
main()
{
X x; return 0;
}
führt zur Ausgabe: 1 3 2
Klassenvariable
C++ erlaubt die Definition und Deklaration von Klassenvariablen mit dem
Schlüsselwort static. Allerdings können diese Elemente, z.B. "stapelgroesse",
nicht innerhalb der Klassendefinition initialisiert werden.
#include <iostream.h>
class intStapel
{
private:
static const int stapelgroesse = 100;
.........
// hier verboten!
"stapelgroesse" muß außerhalb der Klasse definiert und bei Bedarf initialisiert
werden.
class intStapel
{
private:
static const int stapelgroesse;
.................................
}
// Deklaration
const int intStapel :: stapelgroesse = 100;
// Definition
Allerdings ist dann "int inhalt[stapelgroesse]" falsch. Ein Ausweg ist:
Verwendung einer Zeigerkomponente anstatt einer "array-"Komponenten.
class intStapel
{
private:
static const int stapelgroesse;
int *inhalt;
int nachf;
public:
intStapel()
: nachf(0),
inhalt(new int[stapelgroesse]) { }
intStapel(int e) : nachf(0),
inhalt(new int[stapelgroesse]) { push(e); }
...............................................................
};
const int intStapel :: stapelgroesse = 100;
160
Die Programmiersprache C++
Destruktoren
Sie werden aufgerufen, falls ein Objekt nicht mehr benötigt wird. Das ist in 4
verschiedenen Situationen der Fall:
1.
2.
3.
4.
Der Block, in dem eine automatische Variable definiert wurde, wird verlassen
Ein über "new" angelegtes Objekt wird mit "delete" zurüchgegeben
Ein Programm, in dem eine statische Variable definiert ist, wird beendet.
Ein vom Übersetzer erzeugtes temporäres Objekt wird nicht nehr benötigt.
Destruktoren sind nicht immer nötig. So gibt es im einführenden Beispiel nichts
wegzuräumen. Dynamisch angelegte Felder sind allerdings der Speicherverwaltung
zurückzugeben.
Destruktoren sind nach der Klasse benannt, ihre Namen beginnen jedoch mit einer
Tilde (~). Sie sind immer parameterlos und verfügen über keinen Ergebnistyp
(ebenso wie Konstruktoren).
class intStapel
{
private:
static const int stapelgroesse;
int *inhalt;
int nachf;
void init() { inhalt = new int [stapelgroesse]; nachf = 0; }
/* Hilfsfunktion zur Sicherstellung, dass die beiden Konstruktoren diesselben Initialisierungen ausführen */
public:
intStapel()
{ init(); }
intStapel(int e) { init(); }
~intStapel() { delete [] inhalt; }
// Destruktor
.....................................................
};
Kopierkonstruktoren
Das folgende Programmstück funktioniert, falls die Implementierung eines Stapel
nach der Vorlage des einführenden Beispiels erfolgt:
intStapel s;
s.push(1);
// s
s.push(2);
// s
intStapel t(s); //
s.push(3);
//
t.push(0);
//
<- 1
<- 1,2
aequivalent zu t = s
s <- 1,2,3
t <- 1,2,0
Die zuletzt eingeführte Variante der Stapelimplementierung führt allerdings auf einen
Fehler: s, t erhalten am Ende diesselben Elemente. Keiner der beiden Konstruktoren
wird hier aufgerufen, da keiner von ihnen ein Stapelargument erwartet. Es erfolgt
(über den Standard-Kopierkonstruktor):
-
Anlegen eines leeren Stapelelements für t
komponentenweises Kopieren, daraus folgt "s.inhalt" und t.inhalt" erhalten diesselbe Adresse.
161
Die Programmiersprache C++
t
s
1
2
0
inhalt
nachf
3
3
~
~
Abb. 3.1-1: Datenstrukturen zum Stapel
Die Fehlerbehebung erfolgt durch einen passenden Kopierkonstruktor (copy
constructor). Er erledigt die übliche Initialisierunsarbeit und kopiert anschließend den
Inhalt seines Arguments auf die neu erzeugte Instanz.
intStapel :: intStapel(const intStapel& s)
// die Referenz auf eine Konstante zeigt, dass der Parameter durch
// den Initialisierungsvorgang typischerweise nicht verändert wird
{
init(); // legt ein neues Feld an
nachf = s.nachf;
for (int i = 0; i < nachf; i++) inhalt[i] = s.inhalt[i];
}
Kopierkonstruktoren werden in folgenden Fällen aktiviert:
-
Explizite Initialisierung eines Objekts durch ein anderes
Initialisierung eines formalen Parameters mit dem Wert des aktuellen Parameters bei
Wertparameterübergaben
Übergabe eines Funktionsergebnisses
Hinweis: Bei der Initialisierung von Referenzen wird kein neues Objekt aufgerufen
und daher auch kein Konstruktor aufgerufen.
Defaultkonstruktoren
Sie werden ohne Argumente aufgerufen. Er kann als einziger zur Initialisierung von
Feldern (arrays) herangezogen werden, z.B.:
intStapel stacks[100]119;
// Initilaisieren aller Tabellenelemente in der Reihenfolge aufsteigender
// Indexwerte
// syntaktisch ist hier nicht moeglich: die Uebergabe von Argumenten
// Die Aktivierung erfolgt nur ueber den Default-Konstruktur
Für Klassen, deren Konstruktor mindestens einen Parameter erwarten, können keine
"Arrays" definiert werden. Bei Klassen, für die überhaupt kein Konstruktor definiert
ist, wird ein "public"-Defaultkonstruktor vom Compiler erzeugt.
Wird stacks[100] eliminiert, wird der Destruktor für alle Feldelemente in
absteigender Reihenfolge aufgerufen.
119
Das Feld stacks[100] wird hier durch sukzessives Aufrufen von intStapel mit 100 leeren Stapeln initialisiert
162
Die Programmiersprache C++
3.1.4 Operatorfunktionen
Bsp.: "Problematische Zuweisung"
intStapel s, t;
s.push(1); s.push(2);
t = s;
// Zuweisung
s.push(3); // s <- 1,2,3
t.push(0); // s<- 1,2,0
s, t (eigentlich unterschiedliche Objekte) benutzen den gleichen Speicherbereich
über die Komponente "inhalt".
Die Lösung des Problems erfolgt über eine Komponentenoperatorfunktion:
class intStapel
{
...........
intStapel& operator = (const intStapel&);
};
// Operatorfunktion (Stapel mit eigenem Zuweisungsoperator)
intStapel& intStapel :: operator = (const intStapel& r)
{
nachf = r.nachf;
for (int i = 0; i < nachf; i++) inhalt[i] = r.inhalt[i];
return *this;
}
Der am weitesten links stehende Operand wird durch *this (wird immer
mitübergeben) repräsentiert, ein n-ärer Operator hat daher nur (n-1) Parameter. Da
die Zuweisung in C++ üblicherweise den Wert der linken Seite liefert, ist es
empfehlenswert, "operator=()" eine Referenz auf *this zurückgeben zu lassen.
Überladen von Operatoren
Damit kann die Bedeutung von Operatoren für die Anwendung auf Objekte
spezifischer Klassen definiert werden. Die klassenspezifische Definition kann
arithmetische, logische, relationale Operatoren, den Zuweisungsoperator, den
Aufruf-Operator (), den Subskript-Operator [] und den "dereference"-Operator ->
umfassen. Zu beachten ist:
-
Die Priorität der Operatoren kann nicht verändert werden
Die Stelligkeit (unär, binär) kann nicht geändert werden
Operatoren, die unär und binär sein können, gelten bei der Redefinition als unterschiedlich
Bsp.: Test auf Gleichheit zweier Stapelobjekte
int intStapel :: operator == (const intStapel& r)
{
if (r.nachf != nachf)
return 0;
// unterschiedliche Groessen
if (r.inhalt == inhalt) return 1;
//
for (int i = 0; i < nachf; i++)
if (inhalt[i] != r.inhalt[i]) return 0; //
return 1;
}
163
Die Programmiersprache C++
3.1.5 Konstante Komponentenfunktionen
Bei der folgenden Definition einer Nichtkomponentenfunktion groesseSt() kommt
es zu einer Zugriffskollision:
inline long groesseSt(const intStapel& s)
{
return sizeof(s) + intStapel::stapelgroesse * sizeof(int);
}
"stapelgroesse" ist private Klassenvariable und nur über Komponentenfunktionen
heraus ansprechbar. Daher muß "groesseSt()" als Komponenetenfunktion
definiert werden:
inline long intStapel :: groesseSt()
{ return sizeof(intStapel) + stapelgroesse * sizeof(int);
}
, die dann mit
inline long groesseSt(const intStapel& s)
{
return s.groesseSt();
// wird vom Compiler als Fehler markiert,
// da die Konstanz von s nicht garantiert werden kann
}
überladen wird.
Nötig ist die explizite Deklaration, daß die Methode "groesseSt()" ihr Objekt
unangetastet läßt. Das geschieht unmittelbar durch Angabe des Schlüsselworts
„const“ nach der Parameterliste
inline long intStapel :: groesseSt() const
{ return sizeof(intStapel) + stapelgroesse * sizeof(int);
164
}
Die Programmiersprache C++
3.1.6 Statische Komponentenfunktionen
In C++ können Komponentenfunktionen "static" vereinbart sein. Diese
Klassenmethoden verlieren aber das implizite Argument "this" und dürfen daher nicht
mehr auf Instanzen Bezug nehmen, können dafür aber objektunabhängig aufgerufen
werden.
Bsp.: Die Komponentenfunktion groesseSt() des vorstehenden Beispiels zeigt
keinen Bezug zu einer Stapelinstanz (und ist damit offenbar so etwas wie eine
Klassenmethode):
class intStapel
{
...........................
public:
..........................
static long groesseSt()
};
inline long intStapel :: groesseSt() const
{ return sizeof(intStapel) + stapelgroesse * sizeof(int);
}
Der Aufruf dieser Funktion kann über ein beliebiges Objekt der Klasse intStapel oder
lediglich unter Angabe des Klassennamens erfolgen.
3.1.7 "friend"-Funktionen und "friend"-Klassen
Die Komponentenfunktion "groesseSt()" ermöglicht den Zugriff einer globalen
Funtion gleichen Namens auf Klasseninterna. Alternativ dazu hätte der Funktion
groessSt() auch explizit der Zugriff auf private Klassenkomponenten gewährt
werden können, wenn sie als sogenannte "friend"-Funktion deklariert worden wäre:
class intStapel
{
private:
....................
public:
......................
friend long groesseSt(const intStapel& s);
};
Auch Klassen können als "friend" anderer Klassen deklariert werden.
Häufig wird "operator <<()" als friend vereinbart:
class intStapel
{
private:
....................
public:
....................
friend ostream& operator << (ostream& o, const intStapel& s);
};
ostream& operator << (ostream& o, const intStapel& s)
{
165
Die Programmiersprache C++
o << "<";
for (int i = 0; i < s.nachf; i++)
{
if ((i <= s.nachf - 1) && (i > 0)) o << ", ";
o << s.inhalt[i];
}
return o << ">";
}
„Friend“-Funktionen weichen das Prinzip des „information hiding“ auf und sollten
deshalb sparsam verwendet werden. Am häufigsten werden sie zusammen mit dem
Mechanismus des „Overloading“ benutzt.
3.1.8 ADT Stapel
Abstrakte (Daten-) Typen werden über die Angabe der Datenelemente und der
Methoden, die auf diesen Datenelementen operieren, beschrieben. Die Methoden
können eingeteilt werden in:
-
Konstruktoren
Sie liefern neue Elemente (Instanzen) des ADT.
-
Zugriffsfunktionen
Sie bestimmen die Eigenschaften von existierenden Datenelementen des Typs
-
Umsetzungsfunktionen
Sie bilden neue Elemente des ADT (durch Hinzufügen bzw. Entfernen)
Bezogen auf den „ganzzahligen Stapel“ führt das zur folgenden Schnittstellenbeschreibung:
#include <iostream.h>
class intStapel
{
private:
static const int stapelgroesse;
int *inhalt;
int nachf;
void init() { inhalt = new int[stapelgroesse]; nachf = 0; }
public:
intStapel()
{ init(); }
// Konstruktoren
intStapel(int e) { init(); push(e); }
intStapel(const intStapel&);
~intStapel() { delete [] inhalt; }
// Destruktor
intStapel& push(int w)
{ inhalt[nachf++] = w; return *this; }
intStapel& pop()
{ nachf--; return *this; }
int top()
{ return inhalt[nachf - 1]; }
int stapeltiefe()
{ return nachf; }
int istleer()
{ return nachf == 0; }
long groesseSt() const
{ return sizeof(intStapel) + stapelgroesse * sizeof(int); }
intStapel& operator = (const intStapel&);
int operator == (const intStapel&);
friend ostream& operator << (ostream& o, const intStapel& s);
};
const int intStapel :: stapelgroesse = 100;
// Kopierkonstruktor
166
Die Programmiersprache C++
intStapel :: intStapel(const intStapel& s)
{
init();
// Anlegen eines neuen Felds
nachf = s.nachf;
for (int i = 0; i < nachf; i++) inhalt[i] = s.inhalt[i];
}
// Operatorfunktion (Stapel mit eigenem Zuweisungsoperator)
intStapel& intStapel :: operator = (const intStapel& r)
{
nachf = r.nachf;
for (int i = 0; i < nachf; i++) inhalt[i] = r.inhalt[i];
return *this;
}
// Operatorfunktion zur Ueberpruefung zweier Stapelobjekte auf Gleichheit
int intStapel :: operator == (const intStapel& r)
{
if (r.nachf != nachf)
return 0;
// unterschiedliche Groessen
if (r.inhalt == inhalt) return 1;
// Fall 1 oder 2
for (int i = 0; i < nachf; i++)
if (inhalt[i] != r.inhalt[i]) return 0; // Fall 3
return 1;
}
// Operator <<()
ostream& operator << (ostream& o, const intStapel& s)
{
o << "<";
for (int i = 0; i < s.nachf; i++)
{
if ((i <= s.nachf - 1) && (i > 0)) o << ", "; o << s.inhalt[i];
}
return o << ">";
}
Anwendung: „Umrechnen von Dezimalzahlen in andere Basisdarstellungen“ 120
Aufgabenstellung: Defaultmäßig werden Zahlen dezimal ausgegeben. Ein Stapel,
der Ganzzahlen aufnimmt, kann dazu verwendet werden, Zahlen bezogen auf eine
andere Basis als 10 darzustellen. Die Funktionsweise der Umrechnung von
Dezimalzahlen in eine Basis eines anderen Zahlensystem zeigen die folgenden
Beispiele:
2810  3  8  4  34 8
72 10  1  64  0  16  2  4  0  1020 4
5310  1  32  1  16  0  8  1  4  0  2  1  1101012
Mit einem Stapel läßt sich die Umrechnung folgendermaßen unterstützen:
120
PR31810.CPP
167
Die Programmiersprache C++
6
leerer Stapel
n  355310
7
7
4
4
4
1
1
1
1
n%8=1
n/8=444
n  444 10
n%8=4
n/8=55
n  5510
n%8=7
n/8=6
n  610
n%8=6
n/6=0
n  010
Abb.3.1-2: Umrechnung von 355310 in 67418 mit Hilfe eines Stapel
Algorithmus zur Lösung der Aufgabe:
1.
2.
3.
4.
Die am weitesten rechts stehende Ziffer von n ist n%b. Sie ist auf dem Stapel abzulegen.
Die restlichen Ziffern von n sind bestimmt durch n/b. Die Zahl n wird ersetzt durch n/b.
Wiederhole die Arbeitsschritte 1. und 2. bis keine signifikanten Ziffern mehr übrig bleiben.
Die Darstellung der Zahl in der neuen Basis ist aus dem Stapel abzulesen. Der Stapel ist zu
diesem Zweck zu entleeren.
Implementierung: Das folgende kleine Testprogramm realisiert den Algorithmus und
benutzt dazu eine Instanz der vorliegenden Klasse intStapel.
void ausgabe(long zahl, int b)
{
intStapel s; // Instanz
// Extrahiere die Ziffern zur jeweiligen Basis (von rechts nach links)
// und lege sie im Stapel ab
do
{
s.push(zahl % b); zahl /= b;
}
while (zahl != 0);
while (!s.istleer())
{
cout << s.top(); s.pop();
}
}
int main(void)
{
long zahl;
//
int b;
// Basis
// lies 3 positive ganze Zahlen und die gewuenschte Basis
for (int i = 0; i < 3; i++)
{
cout << "\nGib eine nicht negative ganze Dezimalzahl und "
<< "\ndanach die Basis (2 <= b <= 9) an" << endl;
cin >> zahl >> b;
cout << zahl << " basis " << b << " ist: ";
ausgabe(zahl,b);
cout << endl;
}
}
168
Die Programmiersprache C++
3.2 Abgeleitete Klassen
3.2.1 Basisklasse und Ableitung
Vorhandene Klassen können zur Definition neuer Klassen herangezogen werden.
Die Klasse, die zur Definition verwendet wird, heißt Basisklasse. Die neue Klasse ist
die abgeleitete Klasse oder Ableitung.
Bsp.:
class aKlasse
{
private:
int A;
protected:
int B;
public:
int C;
void f();
};
Die folgende Deklaration
class abKlasse : public aKlasse
{
public:
void g();
};
leitet eine neue Klasse (abKlasse) aus der alten Klasse (aKlasse) ab. "abKlasse"
beerbt "aKlasse" und darf alle öffentlichen (public) und geschützten (protected)
Bereiche benutzen.
Private Bereiche dürfen nicht verwendet werden. So ist bspw. innerhalb der
Elementfunktion g() die Anweisung "A = 10;" nicht erlaubt. "B = 10; C = 20;" sind
dagegen gestattet.
In einer privaten Ableitung erhalten alle geerbten Klassenmitglieder den Status
private.
"Friend"-Deklarationen werden nicht vererbt.
Konstruktoren (und Destruktoren) nehmen bei abgeleiteten Klassen eine
Sonderstellung ein: Sie werden ebenfalls nicht vererbt.
169
Die Programmiersprache C++
3.2.2 Einfache Vererbung
Bsp.: "Abgesicherter Stapeltyp"
Die Methoden von intStapel sollen mit einer Fehlerbehandlung ausgestattet werden.
Der abgesicherte Stapeltyp (mit Fehlerbehandlung) ist eine Spezialisierung des
vorliegenden Stapeltyps. Die erweiterten Operationen sollen folgendermaßen
gestaltet sein:
1. Überprüfung der Voraussetzung für die eigentliche Operation
2. Durchführung der eigentlichen Operation
3. Kontrolle, ob die Durchführung erfolgreich war
Die folgende Darstellung121 beschreibt die vom bisher vorliegenden Stapeltyp
abgeleitete Spezialisierung:
class PruefeintStapel : public intStapel
{
static int abbruch;
int fehler;
// Fehlerstatus eines Objekts
int test(int bed, char* ort);
public:
PruefeintStapel();
PruefeintStapel(int);
PruefeintStapel (const PruefeintStapel&);
PruefeintStapel& push(int w);
PruefeintStapel& pop();
int top();
PruefeintStapel& operator = (const PruefeintStapel&);
// long groesseSt() const;
};
int PruefeintStapel::abbruch = 0;
Die private Komponentenfunktion test() überprüft die Bedingung "bed" und gibt im
Fehlerfall eine Meldung aus:
// Programmabbruch
extern "C" void exit(int);
// Private Elementfunktion test() zur Ueberpruefung der Bedingungen
int PruefeintStapel :: test( int bed, char* ort)
{
if (!fehler && !bed)
{
cerr << "Fehler in " << ort << "; this = " << long(this) << "\n";
if (abbruch) exit(1);
// Beendigung des Programms
else fehler = 1;
// Markieren des fehlerhaften Objekts
}
return fehler;
}
Zugriffsfunktionen: pop(), top() und push()
// Zugriffsfunktionen pop()
PruefeintStapel& PruefeintStapel :: pop()
121
PR22306.CPP
170
Die Programmiersprache C++
{
if (!test(stapeltiefe() > 0, "pop"))
intStapel::pop();
return *this;
// Vorbedingung
}
// Zugriffsfunktion top()
int PruefeintStapel :: top()
{
if (!test(stapeltiefe() > 0, "top"))
return intStapel::top();
// Eigentliche Operation
return 0;
// Fehlerfall: beliebiges Ergebnis
}
// Zugriffsfunktion push()
PruefeintStapel& PruefeintStapel::push(int w)
{
if (!test(stapeltiefe() < stapelgroesse, "push"))
// Der Zugriff auf "stapelgroesse" ist noch nicht erlaubt
intStapel::push(w);
return *this;
}
Es fehlt noch die Definition von operator=()
PruefeintStapel& PruefeintStapel :: operator = (const PruefeintStapel& r)
{
intStapel::operator = (r);
fehler = r.fehler;
return *this;
}
Alle übrigen Methoden werden von den Neuerungen in "PruefeintStapel" nicht
berührt und werden im Rahmen der Ableitung der Basisklasse geerbt. Konstruktoren
sind allerdings nicht erblich. Allerdings wird häufig die Hauptarbeit vom Konstruktor
(bzw. Destruktor) der Basisklasse übernommen, der vor der ersten (bzw. der letzten)
Anweisung eines Konstruktors (Destruktors) der abgeleiteten Klasse vom Compiler
automatisch aufgerufen wird.
// Defaultkonstruktor
PruefeintStapel::PruefeintStapel() : fehler(0)
{
test(inhalt != 0, "PruefeintStapel()"); // Nachbedingung
}
PruefeintStapel::PruefeintStapel(int e) : intStapel(e), fehler(0)
{
test (inhalt != 0, "PruefeintStapel(int)");
}
PruefeintStapel::PruefeintStapel(const PruefeintStapel& s)
: intStapel(s), fehler(s.fehler)
{
test(inhalt != 0, "PruefeintStapel(const PruefeintStapel&)");
}
Ein neuer Destruktor wird nicht benötigt. Der vom Compiler ersatzweise zur
Verfügung gestellte Destruktor ruft den Destruktor der Basisklasse auf und sorgt
damit für die korrekte Rückgabe von "inhalt".
171
Die Programmiersprache C++
Noch nicht gelöst ist die Zugriffsmöglichkeit auf Komponenten der Basisklasse.
Mögliche Fehlerbehebungsmaßnahmen sind:
-
-
"stapelgroesse" und "inhalt" in "intStapel" public deklarieren (Verstoß gegen das
Geheimnisprinzip)
Deklaration von "PruefeintStapel" in "intStapel" als friend. Damit wären alle privaten
Komponenten von "intStapel" an "PruefeintStapel" ausgeliefert. Das würde auch für die
"friend"-Deklaration der Methoden "PruefeintStapel::push()" usw. gelten.
Das Zugriffskontrollattribut protected erlaubt den Zugriff der Unterklassen.
Die Definition von intStapel muß dazu folgendermaßen geändert werden:
class intStapel
{
private:
int nachf;
void init() { inhalt = new int[stapelgroesse]; nachf = 0; }
protected:
static const int stapelgroesse;
int* inhalt;
public:
.........................
};
172
Die Programmiersprache C++
3.2.3 Klassenhierarchien
Von einer Klasse können andere Klassen abgeleitet werden. Eine Ableitung kann
außerdem wieder für weitere Klassen verwendet werden.
1. Konstruktoren in Klassenhierarchien
Konstruktoren werden nicht automatisch veerbt, z.B.122
#include <iostream.h>
class aKlasse
{
int A;
public:
aKlasse(int Ain)
{
A = Ain;
cout << "Aufruf Konstruktor aKlasse A = " << A << endl;
}
~aKlasse(void)
{ cout << "Destruktor aKlasse" << endl; }
};
class abKlasse : public aKlasse
{
int B;
public:
abKlasse(int Ain, int Bin):aKlasse(Ain)
{
B = Bin;
cout << "Aufruf Konstruktor abKlasse B = " << B << endl;
}
};
class abcKlasse : public abKlasse
{
int C;
public:
abcKlasse(int Ain, int Bin, int Cin):abKlasse(Ain, Bin)
{
C = Cin;
cout << "Aufruf Konstruktor abcKlasse C = " << C << endl;
}
};
main()
{
abcKlasse b(10,20,30);
return 0;
}
Das Ergebnis eines Aufrufs von main() ist:
Aufruf Konstruktor aKlasse
A = 10
Aufruf Konstruktor abKlasse B = 20
Aufruf Konstruktor abcKlasse C = 30
122
PR22312.CPP
173
Die Programmiersprache C++
Der Basisklassenkonstruktor muß explizit angegeben werden. Ausnahme: Es
handelt sich um den Standardkonstruktor bzw. Defaultkonstruktor, z.B. 123:
#include <iostream.h>
class aKlasse
{
int A;
public:
aKlasse(void)
{
A = 0;
cout << "Standardkonstruktor aKlasse A = " << A << endl;
}
};
class abKlasse : public aKlasse
{
int B;
public:
abKlasse(int Bin)
{
B = Bin;
cout << "Konstruktor abKlasse B = " << B << endl;
}
};
main()
{
abKlasse b(20);
return 0;
}
Das Ergebnis eines Aufrufs von main() ist:
Standarkonstruktor aKlasse A = 0
Konstruktor abKlasse B = 20
Definiert eine Klasse überhaupt keinen Konstruktor, ergänzt der Compiler
selbstständig einen Standardkonstruktor, z.B.124:
#include <iostream.h>
class aKlasse
{
int A;
};
class abKlasse : public aKlasse
{
int B;
public:
abKlasse(int Bin)
{
B = Bin;
cout << "Konstruktor abKlasse B = " << B << endl;
}
};
main()
{
abKlasse b(20);
return 0;
}
123
124
PR22316.CPP
PR22317.CPP
174
Die Programmiersprache C++
Das Ergebnis eines Aufrufs von main() ist: Konstruktor abKlasse B = 20
2. Destruktoren in Klassenhierarchien
In den meisten Fällen wird hier die Arbeit vom Destruktor der Basisklasse
übernommen, die nach der letzten Anweisung eines Destruktors der abgeleiteten
Klasse automatisch (vom Compiler) aufgerufen wird, z.B.125:
#include <iostream.h>
class aKlasse
{
int A;
public:
~aKlasse(void) { cout << "Destruktor aKlasse" << endl; }
};
class abKlasse : public aKlasse
{
int B;
};
main()
{
abKlasse b;
return 0;
}
Beim Ende von main() wird b eliminiert. "abKlasse" übernimmt den Destruktor von
"aKlasse" (Programmausdruck: "Destruktor aKlasse").
Der Unterschied zu normalen Elementfunktionen zeigt sich, falls "abKlasse" einen
eigenen Destruktor definiert, z.B.:
#include <iostream.h>
class aKlasse
{
int A;
public:
~aKlasse(void) { cout << "Destruktor aKlasse" << endl; }
};
class abKlasse : public aKlasse
{
int B;
public:
~abKlasse(void) { cout << "Destruktor abKlasse" << endl; }
};
main()
{
abKlasse b;
return 0;
}
Am Ende von main() wird zuerst der Anweisungsteil des Destruktors von
"abKlasse" ausgeführt. Danach wird der Destruktor der Basisklasse "aKlasse"
aufgerufen.
Grundsätzlich werden Destruktoren immer in umgekehrter Reihenfolge wie die
Konstruktoren aufgerufen, z.B.126:
125
PR22318,CPP
175
Die Programmiersprache C++
#include <iostream.h>
class aKlasse
{
int A;
public:
aKlasse(int Ain)
{
A = Ain;
cout << "Aufruf Konstruktor aKlasse A = " << A << endl;
}
~aKlasse(void)
{ cout << "Destruktor aKlasse" << endl; }
};
class abKlasse : public aKlasse
{
int B;
public:
abKlasse(int Ain, int Bin):aKlasse(Ain)
{
B = Bin;
cout << "Aufruf Konstruktor abKlasse B = " << B << endl;
}
~abKlasse(void)
{ cout << "Destruktor abKlasse" << endl; }
};
class abcKlasse : public abKlasse
{
int C;
public:
abcKlasse(int Ain, int Bin, int Cin):abKlasse(Ain, Bin)
{
C = Cin;
cout << "Aufruf Konstruktor abcKlasse C = " << C << endl;
}
~abcKlasse(void)
{ cout << "Destruktor abcKlasse" << endl; }
};
main()
{
abcKlasse b(10,20,30);
return 0;
}
Der Aufruf von main() ergibt:
Aufruf Konstruktor aKLasse
A = 10
Aufruf Konstruktor abKlasse B = 20
Aufruf Konstruktor abcKlasse C = 30
Destruktor abcKlasse
Destruktor abKlasse
Destruktor aKlasse
126
PR22312.CPP
176
Die Programmiersprache C++
3.2.4 Virtuelle Funktionen
Sie ermöglichen in der Basisklasse die Deklaration von Funktionen (Schlüsselwort
virtual), die in der abgeleiteten Klasse redefiniert werden. Compiler und Lader
garantieren die exakte Übereinstimmung. Prinzipiell können alle Methoden außer
Konstruktoren und statische Funktionen virtuell sein.
Bsp.127:
#include <iostream.h>
class aKlasse
{
private:
char* sx;
public:
virtual void f(void);
/* virtual zeigt an: Diese Funktion kann verschiedene Versionen
in verschiedenen abgeleiteten Klassen haben. Der Ergebnistyp
(hier void) darf in abgeleiteten Klassen nicht umdefiniert
werden */
};
class abKlasse : public aKlasse
{
private:
char* sy;
public:
virtual void f(void);
};
void aKlasse::f(void)
{
sx = "aKlasse";
cout << sx << " aufgerufen " << endl;
}
void abKlasse::f(void)
{
sy = "abKlasse";
cout << sy << " aufgerufen " << endl;
};
main()
{
aKlasse *az1 = new aKlasse;
abKlasse *az2 = new abKlasse;
az1 = az2;
az1->f();
return 0;
}
Das Programm gibt aus: "abKlasse aufgerufen"
Der Typ des Objekts (Instanz) bestimmt, welche Funktion aufgerufen wird. Das gilt
auch für "z", falls main() folgende Gestalt annimmt:
int main()
{
aKlasse *z = new aKlasse;
z->f();
delete z;
z = new abKlasse;
z->f();
delete z;
127
PR22308.CPP
177
Die Programmiersprache C++
return 0;
}
Wegen der erweiterten Zuweisungskompatibilität kann "z" auf alle Klassen der
Hierarchie zeigen. Je nachdem, auf welches Objekt "z" gerade zeigt, wird die
entsprechende Routine "f()" der Klasse aufgerufen. "z" zeigt auf etwas, das während
des Programms viele verschiedene Gesichter annehmen kann. Man spricht von
"Polymorphie" *) (vom griechischen "poly morphos"), d.h.: Vielgestaltigkeit.
Eine virtuelle Funktion wird normal vererbt. Falls sie in einer Ableitung redefiniert
werden soll, muß sie erst mit exakt identischer Parameterliste angegeben werden.
Eine nicht virtuelle Funktion kann in einer Ableitung virtuell deklariert werden. Der
umgekehrte Weg ist nicht möglich.
Virtuelle Konstruktoren gibt es nicht, da zur ordnungsgemäßen Konstruktion
Informationen über den genauen Typ benötigt werden. Auch Zeiger auf
Konstruktoren sind nicht erlaubt.
Ist eine Funktion virtuell definiert, dann wird die Zuordnung zwischen Prozeduraufruf
und aufgerufener Prozedur tatsächlich erst zur Laufzeit des Programm hergestellt.
Diesen Vorgang nennt man late binding. Im Gegensatz dazu bedeutet "early
binding", daß die Zuordnung bereits zur Übersetzungszeit vorgenommen wird.
Eine Klasse mit virtuellen Funktionen hat einen größeren Datenbereich, z.B.
#include <iostream.h>
class aKlasse
{
int A;
void f() {}
};
class bKlasse
{
int B;
virtual void f(){}
};
int main()
{
aKlasse a;
bKlasse b;
cout << "Groesse a: " << sizeof(a) << endl;
cout << "Groesse b: " << sizeof(b) << endl;
}
Der Unterschied in der Ausgabe zum vorliegenden Programm ist durch eine
zusätzliche Zeigervariable verursacht, die der Compiler zur Verwaltung der virtuellen
Funktionen der Klasse anlegt. Der Zeiger zeigt auf die virtual function pointer table
(vtbl)", die die für das "late binding" erforderliche Funktionsadresse enthält.
Bsp.: "intStapel" mit virtuellen Methoden
#include <iostream.h>
class intStapel
{
private:
int nachf;
void init() { inhalt = new int[stapelgroesse]; nachf = 0; }
protected:
static const int stapelgroesse;
178
Die Programmiersprache C++
int* inhalt;
public:
intStapel()
{ init(); }
intStapel(int e) { init(); push(e); }
intStapel(const intStapel&);
~intStapel() { delete [] inhalt; }
// Destruktor
virtual intStapel& push(int w)
{ inhalt[nachf++] = w; return *this; }
virtual intStapel& pop()
{ nachf--; return *this; }
virtual int top()
{ return inhalt[nachf - 1]; }
int stapeltiefe() const
{ return nachf; }
int istleer()
{ return nachf == 0; }
virtual intStapel& operator = (const intStapel&);
int operator == (const intStapel&);
friend ostream& operator << (ostream& o, const intStapel& s);
};
// Unterklasse PruefeintStapel
class PruefeintStapel : public intStapel
{
static int abbruch;
int fehler;
// Fehlerstatus eines Objekts
int test(int bed, char* ort);
public:
PruefeintStapel();
PruefeintStapel(int);
PruefeintStapel (const PruefeintStapel&);
intStapel& push(int w);
intStapel& pop();
int top();
intStapel& operator = (const intStapel&);
};
Das Schlüsselwort virtual kann, muß aber nicht im Rahmen der Redefinition
wiederholt werden. Alle Funktionen, die virtuelle Funktionen redefinieren sollen,
müssen die gleiche Signatur besitzen. Allerdings müßte das Argument von
"intStapel& operator = (const intStapel&)" sinnvollerweise "const
PruefeintStapel&" sein.
So ist "s" bspw. ein "PruefeintStapel" nach folgender Anweisungsfolge:
...
intStapel s;
PruefeintStapel ps;
s = ps;
...
Im Rumpf der virtuellen Funktion wird der Parameter der lt. Deklaration vom
Basisdatentyp ist, in Wirklichkeit aber der abgeleiteten Klasse angehört, auf den
eigentlichen Datentyp konvertiert.
// operator =()
intStapel& PruefeintStapel :: operator = (const intStapel& r)
{
const PruefeintStapel& rh = (PruefeintStapel&) r;
intStapel::operator = (r);
fehler = rh.fehler;
return *this;
179
Die Programmiersprache C++
}
3.2.5 Abstrakte Klassen
Es gibt Klassen mit rein virtuellen Funktionen (abstrakte Klassen)128. Eine derartige
Funktion ist nur deklariert, nicht definiert. Klassen mit rein virtuellen Funktionen
dienen nur zur Definition von Ableitungen. Eine abstrakte Klasse ist nur dann
sinnvoll, falls in ihr mindestens eine Klasse abgeleitet wird, die die fehlende Funktion
bzw. fehlenden Funktionen definiert, z.B.129:
#include <iostream.h>
class intStapel;
class AbsintStapel
{
public:
virtual intStapel& push(int)
virtual intStapel& pop()
virtual int top()
virtual int stapeltiefe()
virtual long groesseSt() const
virtual intStapel& operator =(const intStapel&)
virtual int operator ==(const intStapel&)
};
=
=
=
=
=
=
=
0;
0;
0;
0;
0;
0;
0;
class intStapel : public AbsintStapel
{
private:
static const int stapelgroesse;
int *inhalt;
int nachf;
void init() { inhalt = new int[stapelgroesse]; nachf = 0; }
public:
intStapel()
{ init(); }
intStapel(int e) { init(); push(e); }
intStapel(const intStapel&);
~intStapel() { delete [] inhalt; }
// Destruktor
intStapel& push(int w)
{ inhalt[nachf++] = w; return *this; }
intStapel& pop()
{ nachf--; return *this; }
int top()
{ return inhalt[nachf - 1]; }
int stapeltiefe()
{ return nachf; }
int istleer()
{ return nachf == 0; }
long groesseSt() const
{ return sizeof(intStapel) + stapelgroesse * sizeof(int); }
intStapel& operator = (const intStapel&);
int operator == (const intStapel&);
friend ostream& operator << (ostream& o, const intStapel& s);
};
Da für eine abstrakte Klasse keine Objekte erzeugt werden können, darf sie nicht als
Funktionsergebnis oder Funktionsargument auftreten, ebensowenig wie sie das
Ergebnis einer expliziten Typkonversion sein darf. Referenzen oder Zeiger auf
abstrakte Klassen sind jedoch zulässig.
128
129
Eine Klasse, die mindestens eine rein virtuelle Funktion besitzt, heißt abstrakte Klasse
PR22310.CPP
180
Die Programmiersprache C++
3.2.6 Mehrfachvererbung
Eine Klasse kann von mehreren Basisklassen gleichzeitig abgeleitet werden
(Mehrfachvererbung, multiple inheritance).
Bsp.: "abKlasse" ist von aKlasse und "bKlasse" abgeleitet. "abKlasse" hat
Funktionen und Daten von "aKlasse" und "bKlasse" geerbt.
aKlasse
bKlasse
abKlasse
Eine Klasse kann auch mehrfach als Basisklasse auftreten, z.B.:
aKlasse
abKlasse
acKlasse
abcKlasse
Abb.:
Bsp.130: Konstruktoren und Destruktoren bei Mehrfachvererbung
#include <iostream.h>
// Konstruktoren und Destruktoren bei Mehrfachvererbung
class aKlasse
{
int A;
public:
aKlasse(int Ain)
{
A = Ain;
130
PR22313.CPP
181
Die Programmiersprache C++
cout << "Aufruf Konstruktor aKlasse A = " << A << endl;
}
~aKlasse(void) { cout << "Destruktor aKlasse" << endl; }
};
class abKlasse : public aKlasse
{
int B;
public:
abKlasse(int Ain, int Bin):aKlasse(Ain)
{
B = Bin;
cout << "Aufruf Konstruktor abKlasse B = " << B << endl;
}
~abKlasse(void) { cout << "Destruktor abKlasse" << endl; }
};
class acKlasse : public aKlasse
{
int C;
public:
acKlasse(int Ain, int Cin):aKlasse(Ain)
{
C = Cin;
cout << "Aufruf Konstruktor acKlasse C = " << C << endl;
}
~acKlasse(void) { cout << "Destruktor acKLasse" << endl; }
};
class abcdKlasse : public abKlasse, public acKlasse
{
int D;
public:
abcdKlasse(int Ain, int Bin, int Cin, int Din)
:abKlasse(Ain,Bin), acKlasse(Ain,Cin)
{
D = Din;
cout << "Aufruf Konstruktor abcdKlasse D = " << D << endl;
}
~abcdKlasse(void) { cout << "Destruktor abcdKlasse" << endl; }
};
main()
{
abcdKlasse b(10,20,30,40);
return 0;
}
Die Ausgabe des vorliegenden Programmbeispiels zeigt:
Aufruf
Aufruf
Aufruf
Aufruf
Aufruf
Konstruktor
Konstruktor
Konstruktor
Konstruktor
Konstruktor
Destruktor
Destruktor
Destruktor
Destruktor
Destruktor
aKLasse
abKlasse
aKlasse
acKlasse
abcdKlasse
A
B
A
C
D
=
=
=
=
=
10
20
10
30
40
abcdKlasse
acKlasse
aKlasse
abKlasse
aKlasse
182
Die Programmiersprache C++
3.2.7 Virtuelle Basisklassen
Im vorstehenden Beispiel wurden Datenelemente von "aKlasse" in der Ableitung
"abcdKlasse" doppelt aufgenommen. Das kann durch sog. virtuelle Basisklassen
(Ableitungen) verhindert werden, z.B.:
#include <iostream.h>
// Konstruktoren und Destruktoren bei Mehrfachvererbung
class aKlasse
{
int A;
public:
aKlasse(int Ain)
{
A = Ain;
cout << "Aufruf Konstruktor aKlasse A = " << A << endl;
}
~aKlasse(void) { cout << "Destruktor aKlasse" << endl; }
};
class abKlasse : virtual public aKlasse
{
int B;
public:
abKlasse(int Ain, int Bin):aKlasse(Ain)
{
B = Bin;
cout << "Aufruf Konstruktor abKlasse B = " << B << endl;
}
~abKlasse(void) { cout << "Destruktor abKlasse" << endl; }
};
class acKlasse : virtual public aKlasse
{
int C;
public:
acKlasse(int Ain, int Cin):aKlasse(Ain)
{
C = Cin;
cout << "Aufruf Konstruktor acKlasse C = " << C << endl;
}
~acKlasse(void) { cout << "Destruktor acKLasse" << endl; }
};
class abcdKlasse : public abKlasse, public acKlasse
{
int D;
public:
abcdKlasse(int Ain, int Bin, int Cin, int Din)
: abKlasse(Ain,Bin), acKlasse(Ain,Cin)
{
D = Din;
cout << "Aufruf Konstruktor abcdKlasse D = " << D << endl;
}
~abcdKlasse(void) { cout << "Destruktor abcdKlasse" << endl; }
};
main()
{
abcdKlasse b(10,20,30,40);
return 0;
}
183
Die Programmiersprache C++
Die Ausgabe des vorliegenden Programmbeispiels zeigt:
Aufruf
Aufruf
Aufruf
Aufruf
Konstruktor
Konstruktor
Konstruktor
Konstruktor
Destruktor
Destruktor
Destruktor
Destruktor
aKLasse
abKlasse
acKlasse
abcdKlasse
A
B
C
D
=
=
=
=
10
20
30
40
abcdKlasse
acKlasse
abKlasse
aKlasse
3.2.8 Generische Datentypen
Sie sind bei der Entwicklung wiederverwendbarer Softwarekomponenten von
besonderer Bedeutung. So ist ein allgemeiner (generischer) Stapeldatentyp, der je
nach Anwendung auf den einen oder anderen Elementdatentyp angepaßt werden
könnte, das eigentliche Ziel. Diesselbe Problematik stellt sich bei allen
Containerdatentypen, z.B.: Mengen, Liste, Warteschlangen usw., die für
unterschiedliche Elementdatentypen definiert werden können.
Bsp.: Eine generische Stapelklasse
In einer abstrakten Basisklasse für Stapelelemente werden die Botschaften definiert,
die ein zu stapelndes Element verstehen muß:
#include <iostream.h>
class stapelEl
{
virtual void ausgabe(ostream&) = 0;
public:
virtual ~stapelEl() {}
// triviale Standardefinition des
// Destruktor
virtual stapelEl* clone() = 0;
friend ostream& operator << (ostream& o, stapelEl& e)
{
e.ausgabe(o);
return o;
}
};
// Generischer Stapel
class Stapel
{
private:
static const int stapelgroesse;
// protected:
stapelEl** inhalt;
// dynamisches Feld von Stapelelement-Zeigern
int nachf;
public:
Stapel() : inhalt(new stapelEl*[stapelgroesse]) , nachf(0) { }
~Stapel();
Stapel& push(stapelEl& w)
184
Die Programmiersprache C++
{ inhalt[nachf++] = w.clone(); return *this; }
Stapel& pop()
{ delete inhalt[--nachf]; return *this; }
stapelEl& top()
{ return *inhalt[nachf - 1]; }
int stapeltiefe() { return nachf; }
int istleer()
{ return nachf == 0; }
};
const int Stapel :: stapelgroesse = 100;
// Zur Verwendung dieser Stapelklase ist mindestens eine
// konkrete Klasse als Elementdatentyp notwendig, z.B.:
class intStapel : public stapelEl
{
int w;
void ausgabe(ostream& o) { o << w;}
public:
intStapel(int i = 0) : w(i) { }
stapelEl* clone() { return new intStapel(w); }
operator int() { return w; };
};
class doubleStapel : public stapelEl
{
double w;
void ausgabe(ostream& o) { o << w; }
public:
doubleStapel(double d = 0) : w(d) { }
stapelEl* clone()
{ return new doubleStapel(w); }
operator double() { return w; }
};
Stapel :: ~Stapel()
{
for (int i = 0; i < nachf; i++)
delete inhalt[i];
delete [] inhalt;
}
// Ein kleines Testprogramm
main()
{
Stapel s;
doubleStapel x = 13, y = 11; intStapel a = 11, b = 13;
s.push(x); s.push(y);
cout << "top = " << s.top() << endl;
s.push(a); s.push(b);
cout << "top = " << s.top() << endl;
return 0;
}
185
Die Programmiersprache C++
3.3 Schablonen
Schablonen (Templates) können als "Meta-Funktionen" aufgefaßt werden, die zur
Übersetzungszeit neue Klassen bzw. neue Funktionen 131 erzeugen.
3.3.1 Klassenschablonen
Mit einem Klassen-Template (auch generische Klasse oder Klassengenerator) kann
ein Muster für Klassendefinitionen angelegt werden. In einer Klassenschablone steht
vor der eigentlichen Klassendefinition eine Parameterliste. Hier werden in
allgemeiner Form „Datentypen“ bezeichnet, wie sie die Elemente einer Klasse
(Daten und Funktionen) benötigen.
#include <iostream.h>
template <class elType, int stapelgroesse>
// Parameter: ElType und stapelgroesse
class Stapel
{
private:
elType inhalt[stapelgroesse]; // Festdimensioniertes Feld
int nachf;
// Index des naechsten freien Elements
public:
Stapel() : nachf(0)
{ }
Stapel<elType, stapelgroesse>& push(const elType& w)
{ inhalt[nachf++] = w; return *this; }
Stapel<elType, stapelgroesse>& pop()
{ nachf--; return *this; }
elType top()
{ return inhalt[nachf - 1]; }
int stapeltiefe() { return nachf; }
int istleer()
{ return nachf == 0; }
};
// Ein kleines Testprogramm
int main()
{
Stapel<int, 100> is; Stapel<double, 20> ds;
is.push(13); is.push(11); is.push(40); ds.push(3.14);
int i = is.top();
cout << i << endl;
double d = ds.top();
cout << d << endl;
is.push(ds.top());
return 0;
}
Jede Instanziierung einer Klassenschablone erzeugt neuen Code für die Methoden
der Klasse. Soll der dafür nötige Speicherbedarf reduziert werden, dann empfiehlt es
sich, Schablonen mit Ableitung von generischen Klassen zu kombinieren.
Die folgende Darstellung zeigt eine Verallgemeinerung des ganzzahlige Stapels132 zu
einer universell einsetzbaren Klassenschablone133. , die Datenwerte beliebiger
Datentypen aufnehmen kann.
131
132
vgl. 1.7.7
vgl. 3.1.8: ADT-Stapel
186
Die Programmiersprache C++
#include <iostream.h>
#include <stdlib.h>
#include <ctype.h>
// Zentrale Fehlerbehandlungsroutine
static void fehler(char* nachricht)
{
cerr << nachricht << endl;
exit(1);
}
template <class T> class Stapel
{
private:
static const int stapelgroesse;
T *inhalt;
int nachf;
void init() { inhalt = new T[stapelgroesse]; nachf = 0; }
public:
Stapel()
{ init(); }
Stapel(T e) { init(); push(e); }
Stapel(const Stapel&);
~Stapel()
{ delete [] inhalt; }
// Destruktor
Stapel& push(T w)
{ inhalt[nachf++] = w; return *this; }
Stapel& pop()
{ nachf--; return *this; }
T top()
{ return inhalt[nachf - 1]; }
int stapeltiefe()
{ return nachf; }
int istleer()
{ return nachf == 0; }
long groesseSt() const
{ return sizeof(Stapel<T>) + stapelgroesse * sizeof(T); }
Stapel& operator = (const Stapel<T>&);
int operator == (const Stapel<T>&);
friend ostream& operator << (ostream& o, const Stapel<T>& s);
};
template <class T> const int Stapel<T> :: stapelgroesse = 100;
// Kopierkonstruktor
template <class T> Stapel<T> :: Stapel(const Stapel& s)
{
init();
// Anlegen eines neuen Felds
nachf = s.nachf;
for (int i = 0; i < nachf; i++)
inhalt[i] = s.inhalt[i];
}
// Operatorfunktion (Stapel mit eigenem Zuweisungsoperator)
template <class T>
Stapel<T>& Stapel<T> :: operator = (const Stapel<T>& r)
{
nachf = r.nachf;
for (int i = 0; i < nachf; i++)
inhalt[i] = r.inhalt[i];
return *this;
}
// Operatorfunktion zur Ueberpruefung zweier Stapelobjekte auf Gleichheit
template <class T>
int Stapel<T> :: operator == (const Stapel<T>& r)
{
133
vgl. auch 5.2.7
187
Die Programmiersprache C++
if (r.nachf != nachf)
return 0;
if (r.inhalt == inhalt) return 1;
for (int i = 0; i < nachf; i++)
if (inhalt[i] != r.inhalt[i]) return 0;
return 1;
// unterschiedliche Groessen
// Fall 1 oder 2
// Fall 3
}
// Platzbedarf fuer Struktur und gestapelte Elemente ermitteln
template <class T> long groesse(const Stapel<T>& s)
{
return s.groesseSt();
}
// Operator <<()
template <class T> ostream& operator << (ostream& o, const Stapel<T>& s)
{
o << "<";
for (int i = 0; i < s.nachf; i++)
{
if ((i <= s.nachf - 1) && (i > 0)) o << ", ";
o << s.inhalt[i];
}
return o << ">";
}
3.3.2 Methodenschablonen
Methoden zu Klassenschablonen können „inline“ oder außerhalb des Kassenkörpers
definiert sein. Eine externe Definition muß als Funktionsschablone behandelt
werden. Danach erfolgt der Name und der Scope-Operator.
Eine Definition von push() außerhalb der Klassenschablone würde dann so
aussehen:
template <class ElType; int sg>
Stapel <ElType,sg>& Stapel<ElType,sg>::push(const ElType& w)
{
inhalt[nachf++] = w; return *this;
}
Die aktuellen Schablonenargumente werden beim aufruf von push() aus dem Typ
des Objekts, auf das die Funktion angewandt wird, abgeleitet:
Stapel <char,20> s;
s.push('X');
Konstanten ändern ihren Namen nicht, z.B.: template <class ElType, int sg>
Stapel <ElType, sg> :: Stapel():nachf(0) {}
188
Die Programmiersprache C++
3.4 Ein-, Ausgabe
3.4.1 Aufbau
Streams
Ein-, Ausgabe sind in C++ nicht unmittelbarer Sprachbestandteil. Sie werden durch
Klassenbibliotheken (z.B. I/O-Stream-Bibliothek) abgedeckt.
Zur Speicherung von Objekten werden besondere Objekte, sog. Streams,
verwendet. Datenströme oder Streams bilden ein sehr leistungsfähiges Konzept zur
Einbindung von Dateien134, externen Ein-/Ausgabegeräten, aber auch von internen
Datenstrukturen in eine Programmierumgebung. Ein Datenstrom ist eine Folge von
Objekten:
... Folge von Objekten ...
Anfang
Position
Ende
Abb. 3.4-1: Datenströme als Folge von Objekten
Zum Lesen und Schreiben benutzt man einen Zeiger, den man in beiden Richtungen
auf dem Datenstrom verschieben kann. Man muß nur die Position des Zeigers sowie
Anfang und Ende des Datenstroms erkennen. Exemplare der Klasse Stream können
Dateien, Eingaben von der Maus und der Tastatur sowie Strings, Arrays und andere
Objekte sein. Auf der untersten Ebene wird ein Stream als eine Folge von Bytes
aufgefaßt.
Das Byte ist die Dateneinheit des Stroms, andere Datentypen wie int, struct,
char* oder vector erhalten erst durch Bündelung und Interpretation von
Bytesequenzen auf höherer Ebene ihre Bedeutung. Die Basisklasse heißt ios_base,
aus ihr werden die anderen Klassen abgeleitet.
134
Ein Stream ist gewissermaßen die objektorientierte Sichtweise einer Datei
189
Die Programmiersprache C++
ios_base
basic_ios
basic_istream
basic_ostrem
basic_iostream
basic_ostringstream
basic_ofstream
basic_istringstream
basic_stringstream
basic_ifstream
basic_fstream
Abb.: Hierarchie der Klassentemplates für Ein- und Ausgabe
Die mit dem Wort „basic“ beginnenden Klassen sind Templates, die für beliebige
Zeichentypen geeignet sind (z.B. Unicode-Zeichen). Für den am häufigsten
vorkommenden Datentyp char wurden die Klassen durch Typdefinitionen wie
typedef basic_ofstream<char> ofstream spezialisiert.
ios_base
ios
istream
istringstream
ifstream
iostream
stringstream
fstream
ostream
ostringstream
ofstream
Abb.: Spezialisierte Klassen für char
Die I/O-Stream-Bibliothek
Die I/O-Stream-Bibliothek (definiert in iostream.h) besteht aus 2 parallelen
Klassenfamilien: Eine ist abgeleitet von streambuf, die andere von ios. Alle StreamKlassen stammen von diesen beiden Klassen ab. Der Zugriff von ios-Klassen auf
streambuf-Klassen erfolgt über Zeiger. Die Klasse ios (und daher auch alle von ihr
abgeleiteten Klassen) enthält einen Zeiger auf ein streambuf-Objekt. Damit führt
sie formatierte Ausgaben und Eingaben bzw. eine Fehlerprüfung durch. streambuf
stellt eine Schnittstelle zum Hauptspeicher und den Perepheriegeräten zur
Verfügung und bietet allgemeine Methoden zum Aufbau und Bearbeiten von
Streams an. Die Methoden der streambuf-Klassenhierarchie werden von den iosKlassen genutzt. Die Klassenhierarchie von streambuf umfaßt: filebuf (Klasse für
Dateipuffer), strstreambuf (Klasse für Zeichenketten-Puffer), stdiobuf (Klasse für
Standard-I/O-Dateipuffer)
Die Klasse iostream kann über Mehrfachvererbung (multiple inheritance) sowohl zur
Eingabe als auch zur Ausgabe verwendet werden.
190
Die Programmiersprache C++
ios
istream
ifstream
ostream
ofstream
iostream
istream_withassign
ostream_withassign
istrstream
ostrstream
fstream
strstream
stdiostream
streambuf
filebuf
strstreambuf
stdiobuf
Abb. 3.4-3: Hierarchie der iostream-Klassen
Die iostream-Bibliothek stellt drei Klassen bereit, die den drei Arten von
Datenströmen entsprechen:
-
istream: Eingabeströme
ostream: Ausgabeströme
iostream: Ein- und Ausgabeströme
Ein-, Ausgabe-Operationen können auf Objekten dieser drei Klassen ausgeführt
werden. Alle Operationen, die für eine der beiden Klassen istream und ostream
erlaubt sind, können auch auf Objekten der Klasse iostream ausgeführt werden.
Bspw. ist << für ostream-Objekte definiert, >> für istream-Objekte. Beide sind
erlaubt für iostream-Objekte.
Vordefinierte Datenströme
sind im Header <iostream> folgendermaßen deklariert
namespace std
{
extern istream
extern ostream
extern ostream
extern ostream
}
cin;
cout;
cerr;
clog;
//
//
//
//
Standardeingabe
Standardausgabe
Standardfehlerausgabe
gepufferte Standardfehlerausgabe
Sie entsprechen den FILE* Dateizeigern stdin, stdout und stderr in C.
191
Die Programmiersprache C++
Eingabe-Klassen
Die allgemeine Eingabe-Klasse ist: istream. Spezielle Klassen existieren für
Eingabedateien (ifstream), für cin (istream_withassign) und für den zu lesenden
Arbeitsspeicher (istrstream).
Ausgabe-Klassen
Die allgemeine Ausgabe-Klasse ist: ostream. Spezielle Klassen existieren für
Ausgabedateien (ofstream), für cout, cerr, clog (ostream_withassign) und für
den zu lesenden Arbeitsspeicher (ostrstream).
Initialisierungsklasse
Sie heißt: Iostream_init und erzeugt cin, cout, cerr und clog. Beim Start eines
C++-Programms sind diese Streams definiert, die folgendermaßen als Objekte von
withassign-Klassen deklariert sind:
extern
extern
extern
extern
istream_withassign
ostream_withassign
ostream_withassign
ostream_withassign
cin;
cout;
cerr;
clog;
//
//
//
//
entspricht stdin, Datei-Deskriptor 0
entspricht stdout, Datei-Deskriptor 1
entspricht stderr, Datei-Deskriptor 2
gepuffertes cerr, Datei-Deskriptor 3
3.4.2 Ausgabe
1. Formatierte Ausgabe erfolgt über
ostream& ostream::operator<<(Argumenten-Typ)
Für den Argumenten-Typ existieren entsprechende Varianten (einschl. char* zur
Zeichenketten- und void* zur Adreßwertangabe). Der Rückgabetyp des Operator
ist eine Referenz auf ostream. Das Ergebnis ist das ostream-Objekt selbst, so daß
ein weiterer Operator darauf angewendet werden kann. Damit ist die
Hintereinanderschaltung von Ausgabeoperatoren möglich. Die <<-Operatoren geben
eine Referenz auf ihren linken Operanden zurück, so dass mehrfache Insertionen
direkt hintereinander möglich sind. Bspw. bedeutet cout << i << j eigentlich
(cout << i) << j. Das Ergebnis von cout << i ist cout im Zustand nach der
Insertion. Dieser Datenstrom ist dann der linke Operand für die Insertion von j.
Je nach auszugebendem Datentyp kann die Standardformatierung auf
unterschiedliche Weise beeinflußt werden.
Standardformatierung durch Formatstatus-Flags über die Methode flags()
Eine ios-Instanzvariable vom Typ long, deren einzelne Bits Formatierungsinformationen tragen, beeinflußt die Darstellungsform. Diverse Formatbits
(Formatstatus-Flags) sind135:
Bezeichnung:
skipws
left, right, internal
Bedeutung:
Trennzeichen136 sind zu ignorieren
Ausrichtung der Ausgabe
135
vgl. Borland C++ für Windows Version 4.0: Referenzhandbuch
Trennzeichen steht für den englischen Begriff „whitespace“ und bedeutet ein beliebiges Zeichen aus der
Menge {‘ ‘,’\t’,’\n’}
136
192
Die Programmiersprache C++
dec, oct, hex
showbase
showpoint
uppercase
showpos
scientific
fixed
unitbuf
stdio
Benutztes Zahlensystem
Ausgabe mit Zahlensystempräfixen (Anzeige der Basis)
Dezimalpunkt wird erzwungen, nachfolgende Nullen werden
ausgegeben
Hexadezimalziffern in Großbuchstaben
führende ‘+’ bei ganzen Zahlen > 0
Gleitpunktnotation (Exponential-Format)
Fixpunktnotation
Leeren der Ausgabepuffer nach jeder Einfügeoperation
Leeren der Puffer von cout, cerr nach jeder Ausgabeoperation
Das Lesen dieser Formatangaben erfolgt mit der Methode: long ios::flags()
Verändert können sie u.a. durch long ios::flags(long) werden (mit Rückgabe
des ursprünglichen Werts des Formatworts als Funktionswert).
Für viele Anwendungsfälle gibt es spezifische Zugriffsfunktionen, z.B.:
ios& ios::dec(ios&)
ios& ios::oct(ios&)
ios& ios::hex(ios&)
// dezimale Ausgabe
// oktale Ausgabe
// hexidezimale Ausgabe
Bsp.: Ausgabe ganzer Zahlen in verschiedenen Zahlensystemen
#include <iostream.h>
int main()
{
cout.flags(cout.flags() | ios :: showbase);
/* setzt das normalerweise ausgeschaltete showbase-Bit,
das die Ausgabe des Zahlensystem-Präfix bewirkt:
0 für oktale, x für hexadezimale Zahlen */
cout << "Dezimal " << 21 << "\n";
oct(cout);
cout << "Oktal " << 21 << "\n";
hex(cout);
cout << "Hexadezimal " << 21 << "\n";
}
Standardformatierung durch einzelne Formatstatus-Flags über setf()
setf() verwendet eine andere Methode für das Setzen der Formatbits
long ios::setf(long setbits, long field)
setzt die durch den Parameter field gesetzten Bits des Formatstatusworts auf das
durch den ersten Parameter (setbits) angegebene Bitmuster. Alle in field nicht
markierten Bits des Formatstatusworts bleiben unberührt.
Soll das vorliegende (alte) Bitmuster nur über „bitweises Oder“ mit dem im ersten
Parameter angegebenen Bitmuster verknüpft werden, dann kann der Parameter
„field“ entfallen.
Bsp.: Ausgabe ganzer Zahlen in verschiedenen Zahlensystemen
#include <iostream.h>
void main()
{
cout.setf(ios::showbase, ios::showbase);
cout << "Dezimal " << 21 << "\n"
<< oct << "Oktal " << 21 << "\n"
<< hex << "Hexadezimal " << 21 << "\n";
}
193
Die Programmiersprache C++
Die folgenden drei Konstanten werden für den 2. Parameter der Funktion setf()
benötigt:
static const long adjustfield;
static const long basefield;
static const long floatfield;
// left | right | internal
// dec | oct | hex
// scientific | fixed
Diese Konstanten dienen im Zusammenhang mit setf()-Aufrufen zur Kontrolle:
- für die Adjustierung von Zeichen
cout.setf(ios::left,ios::adjustfield);
cout.setf(ios::right,ios::adjustfield);
cout.setf(ios::internal,ios::adjustfield);
Die Aufrufe positionieren die auszugebenden Zeichen innerhalb des über
ios::width() angegebenen Felds.
- für die Ausgabe von Fließkomma-Werten
cout.setf(ios::scientific,ios::floatfield);
cout.setf(ios::scientific,ios::floatfield);
Als Standard werden Ziffern ausgegeben, wobei n über cout.precision(n)
gesetzt wird. „precision()“-Angaben gelten bis zum nächsten precision()Aufruf.
- der Basis für die Bestimmung ganzer Zahlen
cout.setf(ios::oct,ios::basefield);
cout.setf(ios::dec,ios::basefield);
cout.setf(ios::hex,ios::basefield);
// oktal
// dezimal
// hexadezimal
Falls explizite Ausgabe der Zahlenbasis gewünscht ist, setzt man
setf(ios::showbase). Das Flag bleibt solange gesetzt, bis es wieder
zurückgesetzt wird, z.B.:
cout << 1234 << ' ';
cout.setf(ios::oct,ios::basefield);
cout << 1234 << ' ';
cout.setf(ios::hex,ios::basefield);
cout << 1234 << ' ';
erzeugt als Ausgabe: 1234 2322 4d2. Mit cout.setf(ios::showbase) vor den
angegebenen Anweisungen, ergibt sich die Ausgabe: 1234 02322 0x4d2.
Manipulatoren
Ein spezieller, funktionsähnlicher Operator (Manipulator) kann zur Änderung von
Formatvariablen herangezogen werden.
Nicht-parametrisierte Manipulatoren (z.B. dec, hex, und oct) nehmen keine
Argumente entgegen und ändern die Konvertierungsbasis dauerhaft, z.B.:
int i = 36;
cout << dec << i << " " << hex << i << " " << oct << i << endl;
cout << dec;
// muß auf Dezimalbasis zurückgesetzt werden
194
Die Programmiersprache C++
Für Formatierungsangaben, die ein Argument benötigen, stehen parametrisierte
Manipulatoren zur Verfügung.
Manipulator
boolalpha
noboolalpha
showbase
noshowbase
showpoint
noshowpoint
showpos
noshowpos
skipws
noskipws
dec
hex
oct
ws
endl
ends
flush
setbase(int n)
Aktion
true/false alphabetisch ausgeben oder lesen
true/false numerisch (1/0) ausgeben oder lesen
Basis anzeigen
Keine Basis anzeigen
Nachfolgende Nullen ausgeben
Keine nachfolgende Nullen ausgeben
+ bei positiven Zahlen ausgeben
Kein + bei positiven Zahlen ausgeben
Zwischenraumzeichen ignorieren
Zwischenraumzeichen berücksichtigen
setzt das Basisformat-Flag für Dezimaldarstellung
setzt das Basisformat-Flag für Hexidezimaldarstellung
setzt das Basisformat-Flag für Oktalkonvertierung
entfernt Whitespace-Zeichen
fügt einen Zeilenvorschub ein und leert den Stream
fügt ein abschließendes Null-Endekennzeichen in einen Stream ein
leert einen ostream
setzt das Basisformat für die Konvertierung zur
Basis n (0, 8, 10, 16). (0 bedeutet Standard: Dezimal
für die Ausgabe, ANSI C-Regeln für literale Integer bei der Eingabe
setzt die durch f angegebenen Format-Bits
löscht die durch f angegebenen Format-Bits
setzt das Füllzeichen auf z
Festlegen der Gleitkomma-Genauigkeit auf n
bestimmt die Feldbreite auf n
setiosflags(long f)
resetiosflags(long f)
setfill(int z)
setprecision(int n)
setw(int n)
Abb. 3.4-4: ios- bzw. iostream-Manipulatoren
#include <iostream.h>
#include <iomanip.h>
void main()
{
int i = 1311, j
cout << setw(6)
cout << setw(6)
setw(6)
}
= 1940, k = 10;
<< i << j << k << endl;
<< i <<
<< j << setw(6) << k << endl;
Eigene Manipulatoren. Die Operatoren << und >> akzeptieren Zeiger auf
Funktionen. Falls die „ausgegebene“ bzw. „eingelesene“ Funktion einen der
folgenden Prototypen hat, so wird nicht der Zeiger ausgegeben, sondern die
Funktion aufgerufen:
ios& f(ios& f);
istream& f(istream& f);
ostream& f(ostream&);
Der Manipulator hex hat bspw. eine Funktion, die so aussieht:
inline ios& hex(ios& i)
{
i.setf(ios::hex, ios::dec|ios::hex|ios::oct);
return i;
}
195
Die Programmiersprache C++
Es ist völlig egal, welche der drei folgenden Anweisungen man benutzt, denn es
passiert immer genau dasselbe:
cout << hex;
cout.operator<< (hex);
hex(cout);
Eigene Manipulatoren müssen lediglich der Schnittstellenkonvention genügen.
Bsp.: Manipulator DM, der dafür sorgt, daß eine nachfolgend ausgegebene
Fließkommazahl in einem ordentlichen Währunssymbol erscheint.
#include <iostream.h>
#include <iomanip.h>
ios &fixed(ios &stream)
{
stream.setf(ios::fixed, ios::floatfield);
return stream;
} // Manipulator fixed137
ostream &DM(ostream &os)
{
os << "DM ";
os.width(10);
os << fixed << setprecision(2);
return os;
} // Manipulator DM
int main()
{
double x = 234.445;
cout << DM << x << endl;
} // main()
Steuerung der Ausgabefeldbreite durch width()
Eine Instanzvariable (vom Typ int) gibt an, wieviele Zeichen von den
Ausgabeoperatoren mindestens übertragen werden. Ist diese Zahl 0, dann werden
immer genau soviele Zeichen ausgegeben, wie notwendig sind, um den
entsprechenden Wert darzustellen. Ist er positiv, werden zu kurze Ausgaben mit
Füllzeichen ergänzt. Die Zahl wird durch die Methode
int ios::width()
gelesen und kann durch
int ios::width(int)
auf einen neuen Wert gesetzt werden, wobei der alte Wert zurückgegeben wird. Die
width-Komponente wird nach jedem Datentransfer, der durch sie beeinflußt wurde,
wieder auf Null gesetzt.
Ändern des Füllzeichens
Das Zeichen138, das zum Auffüllen des Ausgabefelds benützt wird, ist ebenfalls in
einer ios-Variablen gespeichert. Es kann durch die Methode char ios::fill()
137
Der Manipulator fixed fehlt beim aktuellen GNU C++ Compiler (Version 2.95), deshalb wird er hier
zusätzlich definiert.
138 normalerweise das Leerzeichen
196
Die Programmiersprache C++
abgefragt und durch char ios::fill(int z) auf den Wert ‘z’ gesetzt werden.
Diese Funktion gibt das ursprüngliche Füllzeichen zurück.
Bsp.:
#include <iostream.h>
void main()
{
int i = 64;
cout.width(6);
cout.fill('#');
cout.setf(ios::left,ios::adjustfield);
// adjustfield bestimmt, welche Bits zu setzen sind
// ios::left bestimmt, wie sie zu setzen sind
cout << i << endl;
}
Festlegen der Ausgabegenauigkeit
Zur Festlegung einer bestimmten Anzahl von Nullkommastellen kann eine weitere
ios-Komponente durch die Methode precision() (Lesen einer Anzahl von
Nachkommastellen)
und
precision(int)
(Setzen
einer
Anzahl
von
Nachkommastellen) benutzt werden. Die Angabe bei precision(int) wird als
Maximum interpretiert, d.h.: Es werden überflüssige Nachkommastellen
abgeschnitten, jedoch rechts keine Nullen angehängt, falls der Nachkommaausdruck zu kurz ist. Das kann aber erzwungen werden, indem das Formatbit
ios::showpoint gesetzt wird.
Beschreibungsform für float- und double-Zahlen
Bei Gleitpunktzahlen ist die Ausgabe im Gleitpunkt- bzw. Fixpunkt-Format möglich.
Dementsprechend
ist
entweder
das
Formatbit
ios::fixed
oder
ios::scientific anzugeben.
Bsp.: „Fälschungssichere Ausgabe von Geldbeträgen“139
a) Ausgabe über setf()
#include <iostream.h>
void main()
{
double betrag = 1311.4;
cout.width(10);
// 10stelliges Betragsfeld
cout.fill('=');
// Fuellzeichen zur Faelschungssicherung
cout.precision(2);
// Genau 2 Nachkommastellen
cout.setf(ios::showpoint, ios::showpoint);
cout.setf(ios::fixed,ios::floatfield);
cout << betrag << "DM";
}
b) Ausgabe über parametrisierte Manipulatoren
#include <iostream.h>
#include <iomanip.h>
int main()
{
double betrag = 1311.4;
cout.setf(ios::showpoint, ios::showpoint);
cout.setf(ios::fixed,ios::floatfield);
139
PR34205.CPP
197
Die Programmiersprache C++
cout << setw(10) << setprecision(2) << setfill('=')
<< betrag
<< "DM";
}
2. Unformatierte (binäre) Ausgabe
Sie überträgt einen Speicherbereich 1:1. Das kann mit
ostream& ostream::put(char z)
für ein Zeichen140 und mit
ostream& ostream::write(const char* s, int n)
für eine beliebige Anzahl von Zeichen erfolgen. Mit dieser Methode können einfache
Datentypen und Strukturen übertragen werden und durch iostream::read()
wieder eingelesen werden.
Prinzip der unformatierten Ausgabe: Es wird ein Zeiger auf den Beginn des
Datenbereichs (Adresse) angegeben. Dabei wird der Zeiger in char* umgewandelt.
Zusätzlich wird die Anzahl der zu übertragenden Bytes angegeben.
Zur Wandlung des Zeigers wird der reinterpret_cast-Operator verwendet.
Dieser Operator verzichtet im Gegensatz zum static_cast-Operator auf jegliche
Verträglichkeitsprüfung, weil hier Zeiger auf beliebige (auch selbstgeschriebene)
Datentypen in den Typ char* umgewandelt werden sollen.
3. Ausgabe auf Dateien
Zur Arbeit mit Dateien werden die Streamklassen ofstream und ifstream verwendet.
Gleichzeitige Ein- und Ausgabe mit einer Datei ist mit Hilfe der Klasse fstream
möglich, die von iostream abgeleitet ist.
ios
istream
iostream
ostream
fstreambase
ifstream
fstream
ofstream
Abb. 3.4.5: ios-Klassen
Die Klassen ofstream, ifstream und fstream sind in fstream.h definiert.
putback(char z) erlaubt einem Programm, ein „ungewolltes“ Zeichen in den Strom zurückzuschreiben,
damit es an anderer Stelle gelesen werden kann.
140
198
Die Programmiersprache C++
Für die Ausgabe auf Dateien wurde die Klasse ofstream von ostream abgeleitet.
Alle bisher behandelten Methoden sind auf ofstream-Objekte anwendbar. Zum
Öffnen und Schließen der zum ofstream-Objekt gehörenden Dateien sind folgende
spezifische Operationen vorgesehen:
void ofstream::open(const char* dateiname, int mode);
void ofstream::close();
"mode" bestimmt verschiedene
kombinierbare) Werte sind:
ios::out
ios::ate
ios::binary
ios::app
Öffnungsstrategien.
Mögliche
(durch
"oder"
//
//
//
//
Datei fuer Ausgabe oeffnen
nach dem Oeffnen auf Dateiende positionieren
Binaermodus
Hängt Daten an- und schreibt immer an das Dateiende
ios::trunc
// verwirft den Inhalt der Datei, falls sie existiert141
ios::nocreate
// Öffnen soll schief gehen, falls die Datei nicht bereits
// existiert
ios::noreplace // Öffen soll schief gehen, falls die Datei bereits
// existiert, es sein denn, dass gleichzeitig ios::ate der
// ios::app angegeben wird
Mehrere dieser Modi können beim Öffnen über bitweises Oder verknüpft werden.
Das Öffnen einer Datei kann auch unmittelbar beim Anlegen einer ofstreamVariablen erfolgen, z.B.:
ofstream file("xxxxx.xxx",ios::out)142
Ob eine Datei offen ist oder nicht, kann mit der (booleschen) Methode is_open()
festgestellt werden. Der Vorgabewert für ofstream-Dateien ist ios::out. Dateien
werden nach Voreinstellung im Text-Modus geöffnet. Das bedeutet: Die Eingabe von
"carriage return/linefeed" wird in '\n'-Zeichen umgewandelt bzw. die
Ausgabe von '\n'-Zeichen in "carriage return/linefeed". Die Angabe
ios::binary öffnet die Datei im binären Mode.
4. Schreiben in Zeichenketten
Es gibt die Klasse ostrstream für das Schreiben in Zeichenketten. Der ostreamKonstruktor verlangt die Angabe einer Größe, damit nicht über das Ende der
Zeichenkette hinaus geschrieben wird. Wenn Daten in den Strom hineingeschrieben
werden, wird kein Begrenzungszeichen (ASCII-Null) hinzugefügt. Wenn man
eines anhängen möchte, muß man das angeben.
Formatierung in Zeichenketten im ANSI/ISO-Standard. Voraussetzung ist das
Vorhandensein der Klasse string, definiert im Header <string>. String-Streams
benötigen keine C-Zeichenkette mehr als Grundlage und können nicht überlaufen.
141
142
implizit, wenn ios::out angegeben ist oder weder ios::ate noch ios::app angegeben wird
Konstruktor
199
Die Programmiersprache C++
3.4.3 Eingabe
1. Formatierte Eingabe erfolgt über:
istream& istream::operator>> (Argumenten-Typ&)
istream& istream::operator>> (char*);
Die Stream-Eingabe verwendet den überladenen Rechtsschiebe-Operator >> und
funktioniert ähnlich wie die Stream-Ausgabe. Der linke Operand von >> ist ein Objekt
der Klasse istream. Der rechte Operand kann ein beliebiger Typ sein, für den die
Stream-Eingabe definiert wurde. Standardmäßig überspringt der Operator >>
Whitespace-Zeichen und liest dann die Zeichen ein, die zum Typ des EingabeObjekts passen. Das Ignorieren von Whitespace-Zeichen wird durch das Flag
ios::skipws in der Formatstatus-Auflistung gesteuert. Es gibt außerdem noch den
speziellen „Ziel“-Manipulator ws, mit dem Whitespace-Zeichen entfernt werden
können. Auf den Typ char besitzt der Opeartor >> die Wirkung, daß WhitespaceZeichen übersprungen und das nächste (Nicht-Whitespace-) Zeichen gespeichert
wird. Auf den Typ char* (String) besitzt der Operator >> die Wirkung: Führende
Whitespace-Zeichen werden ignoriert, dann werden (Nicht-Whitespace-) Zeichen
eingelesen, bis wieder ein Whitespace-Zeichen folgt. Danach wird ein
abschließendes Null-Endekrtrium angehängt. Ein Überlauf des „char-Arrays“ kann
durch die ios-Methode width() bzw. durch den Manipulator setw auf die
gewünschte Eingabefeldbreite eingeschränkt werden. Wird durch setw die Breite
auf ‘b’ beschränkt, dann werden maximal (b-1) Zeichen übertragen, da das
abschließende Null-Zeichen auch noch untergebracht werden muß.
Jeder Eingabevorgang überspringt normalerweise erst evtl. vorliegende
Trennzeichen, liest dann alle Zeichen vom Stream, die zur Darstellung eines Werts
vom entsprechenden Datentyp gehören, wandelt die Darstellung in eine interne
Repräsentation um und belegt den Referenzparameter, also den rechten Operanden
des Eingabeoperators, mit diesem Wert. Sollte dabei ein Fehler auftreten, werden
entsprechende Fehlerbits gesetzt:
ios::failbit
ios::badbit
ios::eofbit
// bedeutet allgemein: Der Einlesevorgang ist gescheitert
// bedeutet: unerlaubte E/A-Operation versucht
// bedeutet: Der Stream war vorzeitig erschöpft
Der nach außen nicht sichtbare Ablauf einer Tastaturabfrage mit „cin >> “ besteht
aus mehreren Schritten:
1. Aufforderung des (Betriebs-) Systems zur Zeichenübergabe
2. Eingabe der Zeichen auf der Tastatur (mit Korrekturmöglichkeit durch die Backspace-Taste). Die
Zeichen werden vom System der Reihe nach in einem besonderen Speicherbereich abgelegt.
3. Abschluß der Eingabe mit der RETURN-Taste. Damit wird das '\n'-Zeichen als
Zeilenendekennung im Tastaturpuffer abgelegt, der Puffer wird durch das System an C++
übergeben.
4. Auswertung des Tastatur-Puffers durch den Operator >> je nach Datentyp der gefragten Variable.
5. Daten, die nach der Auswertung übrig bleiben, weil sie nicht zu dem Datentyp passen, verbleiben
im Tastaturpuffer und können mit dem nächsten „cin >> ...“ gelesen werden.
2. Für die unformatierte Eingabe stehen verschiedene Methoden zur Verfügung, die
unabhängig vom skipws-Status keine Trennzeichen überlesen.
200
Die Programmiersprache C++
int istream::get()
liest ein Zeichen, und gibt es als int-Wert entsprechend seiner Position in der ASCIITabelle zurück. Bei EOF wird „-1“ zurückgeliefert, z.B.
char z;
while ((c=cin.get()) != EOF)
{
// Verarbeitung von z
}
istream& istream::get(char& z)
liest ein Zeichen und überträgt es in die Variable z. Der Rückgabewert ist dann der
Datenstrom, aus dem gelesen wird. Dessen boolscher Wert kann dann wieder
geprüft werden, z.B.:
char z;
while(cin.get(z))
{
// Einlesen mit Prüfung auf Fehler bzw. Dateiende
// Verarbeitung von z
}
istream& istream::get(char* s, int n, char delim = '\n')
überträgt der Reihe nach Zeichen in den durch „s“ angegebenen Puffer, bis eine der
folgenden Bedingungen auftritt, die in der angegebenen Reihenfolgegeprüft werden:
a) n-1 Zeichen wurden übertragen
b) ein Lesefehler ist aufgetreten
c) das nächste zu lesende Zeichen ist „delim“ (Begrenzungs-, Trennzeichen)
In jedem Fall wird die gelesene Zeichenkette durch ein Nullbyte abgeschlossen.
istream& istream::getline(char*s, int n, char delim = '\n')
entspricht get(), überträgt jedoch auch noch das Zeichen „delim“, falls die 3.
Bedingung auftritt143. Wird der Lesevorgang aufgrund der 1. Bedingung
abgebrochen, dann wird „ios::failbit“ gesetzt. dadurch wird angedeutet: Das
gewünschte Trennzeichen wurde nicht gefunden. Der Aufruf von getline() liest,
bis maximal n-1 Zeichen oder bis ein Zeilen-Endezeichen (oder das Dateiende)
erreicht werden. Die Methode schreibt immer ein Null-Terminierungszeichen an das
Ende der Zeichenkette. Nach einem Einlevorgang mit getline() kann man mit der
Methode gcount() abfragen, wie viele Zeichen verarbeitet wurden.
int peek()
erlaubt einem Programm, das nächste zu lesende Zeichen zu untersuchen, ohne
daß das Resultat der folgenden Operation beeinflußt wird.
istream& istream::ignore(int n = 1, int delim = EOF)
überliest n Zeichen, bricht jedoch auch ab, nachdem „delim“ gelesen wurde. Gilt „n
== MAXINT“ wird nur auf „delim“ geachtet.
istream& istream:: read(char* s, int n)
(Gegenstück zu ostream::write()). Liest n Zeichen und überträgt diese in die
Zeichenkette „s“. Es werden keine Zeichen überlesen, der Abschluß durch ein
Nullbyte unterbleibt. Kann die Anforderung nicht erfüllt werden, wird das
ios::failbit gesetzt.
143
Das Trennzeichen wird gelesen, jedoch nicht in den Puffer übernommen.
201
Die Programmiersprache C++
int istream::gcount()
gibt die Anzahl der zuletzt übertragenen Zeichen zurück, falls es sich dabei um eine
unformatierte Eingabe gehandelt hat
3. Eingabe aus Dateien
Die Methoden open(), close() und is_open() (sowie die Konstuktoren und der
Destruktor) entsprechen ihren ofstream-Gegenstücken.
Öffnungsstrategien sind:
ios::in
ios::binary
//Standard
4. Eingabe aus Zeichenketten
Die Klasse istrstream dient zur Extraktion einzelner Teile aus einer Zeichenkette.
Sie verfügt über 2 Konstuktoren:
istrstream::istrstream(char* s, int n)
Der Stream wird mit einer maximal n Zeichen langen Zeichenkette verknüpft.
istrstream::istrstream(char* s)
In diesem Fall wird angenommen, daß die dem Stream zugeordnete Zeichenkette
s durch Nullzeichen abgeschlossen ist.
Der Zugriff auf den Inhalt eines istrstream-Objekts kann durch die Methode str()
erfolgen.
202
Die Programmiersprache C++
3.4.4 Formatierung in Zeichenketten
3.4.4.1 strstream für C++ im AT&T-Standard
Die Header-Datei strstream.h enthält die notwendigen Methoden zur formatierten
Ausgabe von Zeichenketten, d.h. das, was man in C mit sprintf() macht. Man
kann damit in binäre Dateien formatiert in Zeichenketten schreiben oder aus ihnen
auch lesen.
3.4.4.2 stringstream für C++ im ANSI/ISO-Standard
stringstream-Klassen sind im Header <sstream> deklariert. Die hier
beschriebenen String-Streams benötigen keine C-Zeichenkette mehr als Grundlage.
Voraussetzung für String-Streams der stringstream-Klassen sind C++-Strings der
Klasse string.
Bsp.:
203
Die Programmiersprache C++
3.4.5 Fehlerzustände
Eine Instanzvariable in jedem ios-Objekt gibt über evtl. Fehlerzustände Auskunft. Sie
kann die im Aufzählungstyp iostate definierten booleschen Zustandsgrößen
annehmen, die Zugriffsmethoden rdstate(() und clear() können den Zustandswert
lesen bzw. schreiben.
enum iostate
{
goodbit = 0x00,
eofbit = 0x01,
failbit = 0x02,
badbit = 0x04
};
//
//
//
//
alles OK
Ende des Streams
letzte Ein- / Ausgabe war fehlerhaft
ungueltige Operation, schwerer Fehler
Ein Stream kann nicht mehr benutzt werden, falls badbit gesetzt ist. Folgende
Elementfunktionen haben Zugriff auf die Statusbits:
Funktion
Ergebnis
iostate rdstate()
bool good()
bool eof()
bool fail()
bool bad()
void clear()
void clear(iostate s)
void setstate(iostate)
Aktueller Status
Wahr, falls gut
Wahr, falls Dateiende
Wahr, falls failbit oder badbit gesetzt ist
Wahr, falls badbit gesetzt ist
Statusbit auf goodbit setzen
Statusbits auf s setzen
Einzelne Statusbits setzen
Zum Abfragen des Fehlerzustands gibt es außerdem die Operatoren
bool ios :: operator !()
ios :: operator void*()
// überladener Negationsoperator
// Typumwandlungsoperator
Diese Operatoren werden vom Compiler zur Auswertung von Bedingungen genutzt.
Sie verwenden die Funktion fail() zum Lesen des Dateistatus.
Damit sind die folgenden Konstruktionen erlaubt:
if (!cin) ....
bzw.
while (cin)
//aequivalent zu if (cin.fail()) ...
// entspricht while (!cin.fail()) ...
Auf End-Of-File kann man zusätzlich mit der Methode eof() prüfen.
204
Die Programmiersprache C++
3.4.6 Positionieren in Dateien
ifstream- und ofstream-Ströme besitzen je einen (logischen) Zeiger, an dem die
nächste Lese- und Schreiboperation stattfindet ("cp", current pointer). Dieser Zeiger
kann vom Programmierer abgefragt und gesetzt werden.
// Streampositionen zum Setzen und Lesen des current pointer
// Lesen und Setzen des Lesezeigers
streampos tellg();
// Lesen und Setzen des Schreibzeigers
streampos tellp();
ostream& seekp(streampos);
Der Zeiger ist vom Typ streampos. Der Typ ist in fstream.h in der Regel als long
definiert.
Arithmetik mit streampos-Werten ist aber als eine spezielle Form der seekp- bzw.
seekg-Funktionen möglich. Falls ein zweiter Parameter für diese Funktion angegeben
ist, wird der erste Parameter als (numerischer) Offset von seinem Ausgangspunkt
gerechnet und der zweite Parameter gibt den Ausgangspunkt an.
// Streamfunktionen zum relativen Setzen des current pointer
// (cp) einer Datei
istream& seekg(streamoff,seek_dir); // Lesezeiger
ostream& seekp(streamoff,seek_dir); // Schreibzeiger
Die für seek_dir möglichen Konstanten sind als Aufzählungstyp in der Klasse ios
definiert:
enum seek_dir
{
beg = 0, // Offset von Dateibeginn rechnen
cw = 1,
// Offset vom augenblicklichen cp rechnen
end = 2
// Offset vom Dateiende rechnen
}
205
Die Programmiersprache C++
3.5 Ausnahmebehandlung
3.5.1 Übliche Fehlerbehandlungsroutinen
Fehler treten häufig in Funktionen auf und können dort nicht behoben werden. Der
Aufruf der Funktion muß in Kenntnis von dem Fehler gesetzt sein, damit der Fehler
abgefangen oder weiter („nach oben“) gemeldet werden kann. Zu einer
Fehlerbehandlung können eine Reihe verschiedener Strategien herangezogen
werden:
-
-
-
Programmtechnisch am einfachsten ist der sofortige Programmabbruch innerhalb der Funktion,
die einen Fehler feststellt. Falls keine Meldungen über den Abbruch ausgegeben werden, ist die
Fehlerdiagnose erschwert.
Üblich ist zur Fehlerbehandlung die Übergabe eines Parameters an den Aufrufer der Funktion, der
Auskunft über Erfolg oder Mißerfolg der Funktion gibt. Der Parameter ist nach jedem
Funktionsaufruf abzufragen.
Eine Funktion kann im Fehlerfall die in der C-Welt übliche globale Variable errno setzen, die dann
abgefragt wird. Globale Variable beeinträchtigen jedoch die Portabilität von Funktionen.
Eine Funktion, die einen Fehler feststellt, kann eine andere Funktion zur Fehlerbehandlung
aufrufen, die gegebenenfalls auch den Programmabbruch herbeiführt.
3.5.2 Schema zur Ausnahmebehandlung
C++ bietet das folgende Schema für eine Ausnahmebehandlung an:
try
{
funktion();
/* Falls funktion() einen Fehler entdeckt, wirft sie eine Ausnahme aus (throw), wobei ein
Objekt fuer einen Anstoß einer geeigneten Fehlerbehandlungsroutine uebergeben werden
kann
*/
}
catch (datenTyp1)
{
// durch ausgeworfenes Objekt vom datenTyp1 ausgewaehlte Fehlerbehandlung
// ...
}
catch (datenTyp2)
{
// durch ausgeworfenes Objekt vom datenTyp2 ausgewaehlte Fehlerbehandlung
// ...
}
// gegebenenfalls weitere catch-Blöcke
// ...
// Fortsetzung des Programms nach Fehlerbehandlung an dieser Stelle
// ...
Bsp.: Die folgende Funktion liest „n“ ganze Zahlen in einen Array ein und berechnet
den Durchschnitt der eingelesenen, ganzen Zahlen.
#include <iostream.h>
#include <except.h>
206
Die Programmiersprache C++
float liesGanzzahlenArray(int* x, int n)
{
float sum = 0;
cout << "Eingabe von " << n << " ganzen Zahlen:\n";
for (int i = 0; i < n; i++)
{
cin >> x[i];
if (cin.fail()) throw i;
sum += x[i];
}
return sum / n;
}
Tritt beim Einlesen ein Fehler auf, dann löst die Funktion über einen Aufruf von
throw eine Ausnahme aus. Die Laufzeitbibliothek des verwendeten Compilers
durchsucht in diesem Fall die Aufrufkette auf dem Stack 144, bis ein zum aktuellen
throw korrespondierender catch gefunden wurde. Der entsprechende catch-Block
fängt ausgelöste Ausnahmen auf und ermöglicht eine Behandlung des aufgetretenen
Fehlers. Bei der Ausführung der throw-Anweisung werden automatisch alle auf dem
Stack angelegten Objekte durch den Aufruf ihres Destruktors gelöscht, und die
Aufrufparameter werden vom Stack entfernt.
Der zu der vorliegenden Funktion übergeordnete Programmteil könnte so aussehen:
int main()
{
float durchschnitt;
int a[10], b[12];
cout << "Ueberpruefe das Geschehen, wenn bei der Eingabe\n"
<< "nicht korrekte Zeichen (Ziffern) eingegeben wurden\n";
try
{
durchschnitt = liesGanzzahlenArray(a,10);
cout << "10 ganze Zahle wurden eingelesen. Durchschnitt: "
<< durchschnitt << endl;
durchschnitt = liesGanzzahlenArray(b,12);
cout << "12 ganze Zahle wurden eingelesen. Durchschnitt: "
<< durchschnitt << endl;
}
catch(int k)
{
cout << "Nur " << k << " ganze Zahlen wurden gelesen\n";
}
cout << "Ende des Programms";
}
Zur Reaktion auf Ausnahmen muß ein Programmteil die Routinen, in denen diese
Ausnahmen auftreten, in einem sog. „try“-Block145 einschließen. Direkt nach dem
try-Block muß ein catch-Block folgen, der die eigentliche Ausnahmebehandlung
vornimmt. Ein catch-Block darf nur direkt nach einem try-Block oder einem anderen
catch-Block stehen.
Zur Reaktion auf alle möglichen Ausnahmen könnte der catch-Block
folgendermaßen gestaltet werden:
catch(...)
{
........
144
Das geworfene Ausnahmeobjekt muß allerdings statisch oder global sein, damit es nicht durch die StackAbwicklung bis zum Fangen der Ausnahme, d.h. die Ausnahmebehandlung zerstört wird.
145 throw, try, catch sind C++-Schlüsselworte
207
Die Programmiersprache C++
}
Durch die Ellipse (...) wird ein catch-Block spezifiziert, der generell alle
Ausnahmen behandelt. Catch-Blöcke mit Ellipse müssen immer die letzten in einer
Folge von catch-Blöcken sein.
Wurde eine Ausnahme aufgefangen, ist sie abgeschlossen. Das bedeutet: kein
weiterer catch-Block wird mit der Ausnahme aufgerufen. Ein catch-Block kann
jedoch eine Ausnahme weiterreichen. Stellt ein derartiger Block fest, daß er den
aufgetretenen Fehler nicht beheben kann, ruft er noch einmal throw auf. Durch die
parameterlose Variante von throw wird die aktuelle Ausnahme erneut ausgelöst. Ist
zur Zeit keine Ausnahme aktiv, wird unexspected aufgerufen.
3.5.3 Exception-Hierarchie
C++ stellt eine Reihe vordefinierter Exception-Klassen zur Verfügung. Alle spezielle
Exception-Klassen erben von der Basisklasse exception.
exception
logic error
runtime_error
bad_alloc
invalid_argument
range_error
bad_typed
length_error
overflow_error
bad_cast
out_of_range
underflow_error
bad_exception
domain_error
Abb.: Exception-Hierarchien
Die Klasse Exception besitzt die Schnittstelle
namespace std
{
class exception
{
private:
// ...
public:
exception() throw();
exception (const exception&) throw();
exception operator = (const exception&) throw();
virtual ~exception throw();
virtual const char* what() const throw();
};
}
throw() nach den Deklarationen bedeutet: Die Klasse wirft selbst keine Exception
aus, da es sonst eine unendliche Folge von Exceptions geben könnte.
Die Methode what() gibt einen Zeiger auf char zurück, der auf eine Fehlermeldung
verweist. Eigene Exception-Klassen können durch Vererbung die Schnittstelle
übernehmen, z.B.:
208
Die Programmiersprache C++
namespace std;
{
class logischer_Fehler : public exception
{
public:
explicit logic_error(const string& argument);
}
}
3.5.4 Besondere Fehlerbehandlungsroutinen
Es kann vorkommen, daß mitten in einer Fehlerbehandlung selbst wieder Fehler
auftreten, Dann werden folgende Funktionen aufgerufen:
terminate()
Die Funktion „void terminate()“ wird u.a. aufgerufen, falls
-
der Exception-Mechanismus keine Möglichkeit zur Bearbeitung einer geworfenen Exception findet
der vorgegebene unexspected_handler() aufgerufen wird
ein Destruktor während des Aufräumens eine Exception wirft
ein statisches (nicht lokales) Objekt während der Konstruktion oder Zerstörung eine Exception wirft
Die Standardimplementierung von terminate() beendet das Programm.
unexspected()
Die Funktion „void unexspected“ wird aufgerufen, falls eine Funktion ihre
Versprechungen nicht einhält und eine Exception auswirft, die in der ExceptionSpezifikation nicht aufgeführt wird. Unexspected() kann nun selbst eine Exception
auslösen, so daß die Suche nach dem geigneten Exception-Handler weitergeht.
Falls die ausgelöste Exception nicht der Exception-Spezifikation entspricht, sind 2
Fälle zu unterscheiden:
1. Die Exception-Spezifikation führt bad_exception auf: Die geworfene Exception wird durch ein
bad_exception-Objekt ersetzt.
2. Die Exception-Spezifikation führt bad_exception nicht auf: terminate() wird aufgerufen
uncaught_exception()
Die Funktion bool uncaught_exception() gibt nach Auswertung der Exception
true zurück, bis die Initialisierung im Exception-Handler abgeschlossen ist oder
unexspected() wegen der Exception aufgerufen wurde. „true“ wird auch
zurückgegeben, wenn terminate() aus irgendeinem Grunde außer dem direkten
Aufruf begonnen wurde.
Benutzerdefinierte Fehlerbehandlungsfunktionen
Die zuvor angegebenen Funktionen können bei Bedarf selbst definiert werden. Dazu
sind standardmäßig zwei Typen für Funktionszeiger definiert:
typedef void (*unexspected handler)();
typedef void (*terminate_handler)();
Es gibt zwei Funktionen, denen die Zeiger auf die selbstdefinierten Funktionen
dieses Typs übergeben werden, um die vorgegebenen Funktionen zu ersetzen:
unexspected_handler set_unexspected(unexpected_handler f) throw();
209
Die Programmiersprache C++
terminate_handler set_terminate(terminate_handler f) throw();
Übergeben werden Zeiger auf selbstdefinierte Funktionen, an die bestimmte
Anforderungen gestellt werden:
- Ein unexspected_handler soll
-- bad_exception oder eine der Exception_Spezifikation genügende Exception auswerfen oder
-- terminate() rufen oder
-- das Programm mit abort() oder exit() beenden.
- Ein terminate_handler soll das Programm ohne Rückkehr an den Aufrufer beenden.
3.5.5 Unbehandelte Ausnahmen
Wird im Laufe eines Programms eine Ausnahme ausgelöst, für die kein
korrespondierender catch-Block verfügbar ist, ruft die Laufzeitbibliothek
unexspected auf. Standardmäßig führt unexspected einen Aufruf der Routine
terminate146 aus, die durch Aufruf von abort das Programm beendet.
Das Standarverhalten von unexspected und terminate kann geändert werden.
Das geschieht über einen Aufruf von set_unexspected. Dadurch wird eine
Behandlungsroutine definiert, die prinzipiell beliebige Aktionen ausführen darf, wobei
jedoch nicht zur originären unexspected-Funktion zurückgesprungen werden darf.
Bsp.:
#include <iostream.h>
#include <except.h>
typedef void (*funk)();
// Definition eines Funktionstyps
void f() throw()
// Verzicht auf Ausnahmen
{
// Soll keine Exceptions ausloesen, macht es aber doch
throw "Die Ausnahme, die die Regel bestaetigt";
}
void nasowas()
// wird anstelle von terminate() benutzt
{
cout << "Damit haben Sie nicht gerechnet" << endl;
throw;
// reaktiviert die aktuelle Ausnahmebedingung
}
void main()
{
/* Installation einer Funktion ueber set_unexpected,die beim Auftreten
einer Exception aufgerufen wird, obwohl sie in der Exception-Behandlung
des auslösenden Objekts nicht aufgeführt ist. Bei dieser Funktion
handelt es sich um eine Funktion ohne Argumente mit dem Rueckgabetyp
void */
funk orig = set_unexpected(nasowas);
/* set_unexpected liefert die zuvor installierte Funktion
zurueck. terminate() wird demnach in orig gemerkt */
try { f(); }
// Behandlung von Ausnahmen aller Art
catch(...)
146
Die Funktionsprototypen und Definitionen dieser Routinen befinden sich in except.h
210
Die Programmiersprache C++
{
cout << "Hoppla!, jetzt komm ich\n";
}
set_unexpected(orig);
// Wiederherstellung des ursprünglichen Status
}
Analog dazu läßt sich terminate() durch eine Aufruf von set_terminate() mit
einem Zeiger auf eine parameterlose void-Funktion durch eine selbstdefinierte
Funktion ersetzen. Die übergebene Funktion wird ab Ersetzungszeitpunkt von
terminate() an Stelle von abort() aufgerufen. Auch hier darf nicht zur
Originalroutine zurückgesprungen werden. Die Funktion terminate() wird nicht
nur von unexspected() aufgerufen, sondern auch aktiviert, wenn
-
für eine Ausnahme keine passende catch()-Routine existiert
der Ausnahmebehandlungsmechanismus den Laufzeit-Stack in einem nicht konsistenten Zustand
vorfindet und ihn daher nicht ordnungsgemäß abbauen kann.
ein beim Stack-Abbau aktivierter Destruktor seinerseits eine Ausnahme signalisiert.
211
Die Programmiersprache C++
4. Templates für Algorithmen und Datenstrukturen
4.1 Darstellung von Algorithmen für Graphen mit sequentiell
gespeicherte Listen
4.1.1 Die Datenstruktur Graph
Grundlagen
Graphen werden häufig zur Darstellung von Problemsituationen herangezogen. So
zeigt der folgende (ungerichtete) Graph die Verkehrsverbindungen zwischen
Städten:
752
604
763
648
504
432
355
Abb. Ein (ungerichteter) Graph
Ein (ungerichteter) Graph besteht aus Knoten und Kanten. Knoten tragen in der
Regel zur Identifikation eine Knoten-Identifizierung. Kanten können gewichtet sein
(z.B. mit Entfernungsangaben).
Beim gerichteten Graphen ersetzen Pfeile die Kanten. Daduch ist die ein Fluß in
Richtung des Pfeils bestimmt. Im gerichteten Graphen wird die Kante durch ein Paar
(Vi,Vj) beschrieben. Vi ist der Start- und Vj der Endknoten. Ein Pfad P(VS,VE) ist eine
Folge von Knoten: VS ist der Startknoten, VE ist der Endknoten und jedes in dem
Pfad aufgeführte Paar ist eine Kante.
Darstellung
Es gibt eine Reihe gebräuchlicher Darstellungen. Am häufigsten sind Adjazensmatrix
und Adjazenstabelle.
212
Die Programmiersprache C++
2
A
B
1
B
4
3
A
5
C
C
E
7
D
E
D
Adjazens-Matrizen:
0
0
0
0
0
2
0
4
0
3
1
5
0
7
0
0
0
0
0
0
0
0
0
0
0
0
1
1
0
0
1
0
0
0
0
1
1
0
0
1
1
0
0
0
0
0
0
0
1
0
Adjazentabellen (-listen)
A
B
2
B
C
C
C
1
A
B
C
5
B
A
C
B
4
C
A
D
C
7
E
B
3
D
E
Adjazensmatrizen und Adjazenstabellen
213
E
C
D
Die Programmiersprache C++
4.1.2 Die STL-Containerklasse vector zur Implemetierung einer
Knotenliste für Graphen
Anstelle des veralteten C-Array stellt die STL eine Klassenschablone (template
class T> vector)147 zur Aufnahme und Verwaltung sequentiell gespeicherter
Daten bereit. Diese STL-Containerklasse wird zur Darstellung eines Graphen
(Knoten in einer sequentiellen Liste) benutzt.
#ifndef GRAPH_CLASS
#define GRAPH_CLASS
#include
#include
#include
#include
<iostream>
<fstream>
<vector>
<queue>
using namespace std;
const int maxGraphSize = 25;
template <class T>
class Graph
{
private:
// Schluesseldaten mit einer Knotenliste, Adjazenzmatrix
// und aktueller Groesse (Knotenanzahl) des Graphen
vector<T> vertexList;
int edge [maxGraphSize][maxGraphSize];
int graphsize;
// Methoden zum Finden eines Knoten und Identifizieren
// seiner Position in der sequentiellen Liste
bool findVertex(vector<T> &L, const T& vertex);
int getVertexPos(const T& vertex);
public:
// Konstruktor
Graph(void);
// Testmethoden
int graphEmpty(void) const;
int graphFull(void) const;
// Zugriffsmethoden
int numberOfVertices(void) const;
int getWeight(const T& vertex1, const T& vertex2);
vector<T>& getKnoten();
vector<T>& getNachbarn(const T& vertex);
// Modifikationsmethoden
void insertVertex(const T& vertex);
void insertEdge(const T& vertex1, const T& vertex2,
int weight);
void DeleteVertex(const T& vertex);
void DeleteEdge(const T& vertex1, const T& vertex2);
// Anwendungsmethoden
void readGraph(char *filename);
int minimumPath(const T& sVertex, const T& eVertex);
// SeqList<T>& DepthFirstSearch(const T& beginVertex);
// SeqList<T>& BreadthFirstSearch(const T& beginVertex);
};
Daten zum Graphen werden aufgenommen in:
- einer sequentiellen Liste: vector<T> vertexList
147
vgl. 2.2.3
214
Die Programmiersprache C++
- in einer zweidimensionalen Matrix: int edge [maxGraphSize][maxGraphSize]
Methoden im Graphen dienen
- zum Einlesen der Knotenidentifikationen und Kantenbewertungen aus einer Datei: void readGraph(char
*filename);. Der Parameter dieser Methode beschreibt einen Dateinamen. Die zugehörige Datei
wird mit der Methode readGraph eingelesen. Die Datei hat bspw. folgenden Aufbau:
5
A
B
C
D
E
14
A
B
A
C
A
D
B
C
D
C
D
E
E
C
B
A
C
A
D
A
C
B
C
D
E
D
C
E
604
604
648
648
752
752
432
432
763
763
504
504
355
355
Diese Datei ist folgendem Graphen zugeordnet:
A
752
604
D
B
648
504
432
763
E
355
C
- zum Auffinden eines Knoten in der Knotenliste bool findVertex(vector<T> &L, const T&
vertex)
Implementierung
// Konstruktor zum Initialisieren der Eintraege in der Adjazenzmatrix
// mit 0, Setzen der Groesse des Graphen auf 0
template <class T>
Graph<T>::Graph(void)
{
for (int i = 0; i < maxGraphSize; i++)
for (int j = 0; j < maxGraphSize; j++)
edge[i][j] = 0;
215
Die Programmiersprache C++
graphsize = 0;
}
// Zählen der Komponenten
template <class T>
int Graph<T>::numberOfVertices(void) const
{
return graphsize;
}
// Test, ob der Graph leer ist
template <class T>
int Graph<T>::graphEmpty(void) const
{
return graphsize == 0;
}
// Durchlaufen der Knotenliste und Ermitteln der Position des im Parameter der Funktion
// angegebenen Knotens in der Knotenliste
template <class T>
int Graph<T>::getVertexPos(const T& vertex)
{
int pos = 0; int i = 0;
for (i = 0; i < vertexList.size(); i++)
{
if (vertexList[i] == vertex)
{
pos = i;
break;
}
}
if (i == vertexList.size())
{
cerr << "getVertex: Der Knoten ist nicht im Graph."
<< endl;
pos = -1;
}
return pos;
}
// Ermitteln der Gewichte (Kantenbewertungen) der Kante zwischen den
// in der Parameterliste der Funktion getWeight() angegebenen Knoten vertex1 und vertex2.
template <class T>
int Graph<T>::getWeight(const T& vertex1, const T& vertex2)
{
int pos1 = getVertexPos(vertex1), pos2 = getVertexPos(vertex2);
if (pos1 == -1 || pos2 == -1)
{
cerr << "getWeight: Ein Knoten ist nicht im Graph."
<< endl;
return -1;
}
return edge[pos1][pos2];
}
// Rueckgabe einer Liste mit allen Knoten der Knotenliste
template <class T>
vector<T>& Graph<T>::getKnoten()
{
vector<T> *l;
l = new vector<T>;
for (int i = 0;i < vertexList.size();i++)
l -> push_back(vertexList[i]);
return *l;
}
216
Die Programmiersprache C++
// Rueckgabe einer Liste mit allen benachbarten Knoten
template <class T>
vector<T>& Graph<T>::getNachbarn(const T& vertex)
{
vector<T> *l;
// Zuweisen leere Liste
l = new vector<T>;
// Positionsbestimmung zur Zeilen-Identifikation in der
// Adjazenzmatrix
int pos = getVertexPos(vertex);
// Terminiere, falls der Knoten nicht in der Knotemliste ist
if (pos == -1)
{
cerr << "getNachbarn: Der Knoten ist nicht im Graph."
<< endl;
return *l; // Rueckgabe leere Liste
}
// Durchlaufe die Zeilen der Adjazenzmatrix und erfasse
// alle Knoten einer nicht mit Null bewerteten Kante
for (int i = 0; i < graphsize; i++)
{
if (edge[pos][i] > 0)
l->push_back(vertexList[i]);
}
return *l;
}
// Aufsuchen eines Knoten in der Knotenliste
template <class T>
bool Graph<T>::findVertex(vector<T> &L, const T& vertex)
{
vector<T>::iterator vecIter;
bool ret = false;
for (vecIter = L.begin();
vecIter != L.end(); vecIter++)
{
if (*vecIter == vertex)
{
ret = true;
break;
}
}
return ret;
}
// Einfügen eines Knoten
template <class T>
void Graph<T>::insertVertex(const T& vertex)
{
if (graphsize + 1 > maxGraphSize)
{
cerr << "Graph ist voll" << endl;
exit (1);
}
vertexList.push_back(vertex);
graphsize++;
}
// Einfügen einer Kante
template <class T>
void Graph<T>::insertEdge(const T& vertex1,
const T& vertex2, int weight)
{
int pos1 = getVertexPos(vertex1), pos2 = getVertexPos(vertex2);
if (pos1 == -1 || pos2 == -1)
{
217
Die Programmiersprache C++
cerr << "insertEdge: ein Knoten ist nicht im Graph."
<< endl;
return;
}
edge[pos1][pos2] = weight;
}
// Einlesen der Daten in den Graphen
template <class T>
void Graph<T>::readGraph(char *filename)
{
int i, nvertices, nedges;
T s1, s2;
int weight;
ifstream f;
f.open(filename, ios::in | ios::nocreate);
if(!f)
{
cerr << "Nicht zu oeffnen " << filename << endl;
exit(1);
}
f >> nvertices;
for (i = 0; i < nvertices; i++)
{
f >> s1;
insertVertex(s1);
}
f >> nedges;
for (i = 0; i < nedges; i++)
{
f >> s1;
f >> s2;
f >> weight;
insertEdge(s1, s2, weight);
}
f.close();
}
4.1.3 Mehrdimemensionale Felder
Ein mehrdimensinales Feld wurde zur Speicherung der Bewertungen der Kanten im
ADT Graph benutzt.
218
Die Programmiersprache C++
4.1.4 Durchlaufen von Graphen mit Hilfe der STL-Containerklassen stack
bzw. queue
4.1.4.1 Tiefensuche (First-Depth Search)
Bei der Verarbeitung von Graphen treten häufig folgende Fragen auf:
- Ist der Graph zusammenhängend?
- Wenn nicht, was sind seine zusammenhängenden Komponenten?
- Enthält der Graph einen Zyklus?
Diese und viele andere Probleme können mit einer Methode gelöst werden, die
Tiefensuche genannt wird und einen natürlichen Weg darstellt, wie im Graphen
systematisch jeder Knoten "besucht" und jede Kante geprüft werden kann.
Suchalgorithmus zur Tiefensuche: Er benutzt eine Liste für die Verwaltung
aufgesuchter Knoten eine Liste und einen Stapel (stack der STL-Containerklasse).
4.1.4.2 Breitensuche (Breadth-First Search)
Benutzt man zur Speicherung der Knoten, die bei der Suche im Graphen
durchlaufen werden, anstatt eines Stapels eine Schlange (z.B. den STL-Container
queue), dann führt das zu einem weiteren Algorithmus für die Traversierung in
Graphen, die Breitensuche genannt wird.
4.1.5 Ermitteln der kürzesten Wege mit Hilfe der STL-Containerklasse
priority_queue
Erstellen einer neuen Klasse (struct) mit dem Namen PathInfo
Objekte dieser Klasse spezifizieren Pfade, die zwei Knoten über eine Kante oder
mehrere Kanten verbinden. Die Gewichte der zwischen den Knoten befindlichen
Kanten werden aufsummiert. Die Pfad-Informationen werden in eine Priority-Queue
der STL-Containerklasse priority_queue abgelegt. Dadurch ist Direktzugriff auf
das Pfadobjekt mit den geringsten Kosten möglich:
template <class T>
{
T startV, endV;
int cost;
bool operator <
{ return cost <
bool operator >
{ return cost >
};
struct PathInfo
(const PathInfo<T> a) const
a.cost; }
(const PathInfo<T> a) const
a.cost; }
219
Die Programmiersprache C++
Algorithmus
Gegeben ist der folgende Graph
4
E
4
A
B
2
8
6
10
4
12
6
6
12
F
D
C
20
14
Abb.:
Startknoten (Ausgangspunkt startV) ist der Knoten A. Das Ziel ist der Endknoten D
endV). Dazwischen soll der kürzeste Weg berechnet werden. Die Arbeitsweise des
Algorithmus soll unter diesen Bedingungen beschrieben werden:
Begonnen wired mit Knoten A. Dem Weg von A nach A wird der Kostenbetrag 0 zugeordnet. Inder
Priority-Queue wird eingetragen: "A nach A 0". Es folgt ein iterativer Prozeß, der von A aus
Nachfolgeknoten untersucht, bis der Endknoten endV erreicht ist. Durch das Einbringen aller der auf
dem Weg zum Endknoten liegenden Knoten (einschl. der Pfadlängen) in die Priority-Queue, kann der
kürzeste Pfad aus der Priority-Queue ausgelesen werden.
Der Wert "A" für endV wird gelöscht, betrachtet werden die Nachbarn von A "B, C, E" als neue
Endknoten. Das ergibt folgende Pfadobjekte:
PfadInfo-Objekte
OA,B
OA,C
OA,E
startV
A
A
A
endV
B
C
E
Kosten
4
12
4
Diese Objekte werden in folgende Reihenfolge in die Priorirty-Queue eingeordnet: "A nach B 4", "A
nach E" 4, "A nach C 12". Im nächsten Schritt wird das PfadInfo-Objekt OA,B aus der Priority-Queue
gelöscht. Der zugehörige Enknoten ("B") wird in die Liste "l" der bereits berücksictigten Knoen
aufgenommen, falls er sich nicht in "l" befindet.
Die Nachbarn von B "A", "C" und "D" werden bestimmt. "A" befindet sich bereits in "l", PfadInfo-Objekte
werden zu "C" und "D" ermittelt.
PfadInfo-Objekte
OB,C
OB,D
startV
B
B
endV
C
D
Kosten
10 = 6 + 4
12 = 4 + 8
Die Priority-Queue umfasst nun 4 Elemente: "A nach E 4", "A nach C 12", "B nach C 10", "B nach D
12". Der Eintrag, der mit der kleinste Pfadlänge verbunden ist, unfaßt: "A – B – C". Die Priority-Queue
enthält folgende Elemente: "A nach E 4", "A nach C 12", "B nach C 10", "B nach D 12".
Betrachtet wird das PfadInfo-Objekt OA,E. Es wird gelöscht, die zugehörigen 4 Kosteneinheiten werden
in das erzeugende PfadInfoObjekt OE,D übernommen (4 + 10 = 14). Die Priority-Queue enthält die
Einträge: "B nach C 10", "A nach 10" 12", "B nach D 12", "E nach D 14".
Im nächsten Löschvorgang ist das Objekt OB,C der kleinste Wert. "C" kann nun "l" hinzugefügt werden,
10 Kosteneinheiten betragen die kleinsten Kosten von "A" nach "C".
Die benachbarten Knoten von "C" sind "B" und "D". "B" wurde schon behandelt, die Priority-Queue
besitzt noch 3 Elemente: "B nach D 12", "E nach D 14", "C nach D 24".
Das Entfernen von OB,D aus der Priority-Queue führt auf den kleinsten Pfad von A nach D mit 12
Kosteneinheiten.
220
Die Programmiersprache C++
Implementierung
template <class T>
int Graph<T>::minimumPath(const T& sVertex, const T& eVertex)
{
// priority queue mit Informationen ueber die Kosten auf dem Pfad
// vom Startknoten
priority_queue< PathInfo<T>,
vector<PathInfo<T> >,
greater<PathInfo<T> > > pq;
// wird benutzt, wenn Pfadinformationen in die
// priority queue eingefuegt oder geloescht werden
PathInfo<T> pathData;
// l ist eine Liste aller Knoten , die von sVertex aus erreichbar sind
// adjL ist die Liste aller Nachbarn, die besucht werden.
// adjLiter wird zum Durchlaufen von adjL benutzt
vector<T> l, adjL;
vector<T>::iterator adjLiter;
T sv, ev;
int mincost;
// Angabe der ersten Eintraege
pathData.startV = sVertex;
pathData.endV
= sVertex;
// Kosten von sVertex nach sVertex betragen 0
pathData.cost = 0;
pq.push(pathData);
// Bearbeite Knoten bis ein kuerzester Weg zum
// Zielknoten gefunden ist oder die priority queue leer ist
while (!pq.empty())
{
// delete a priority queue entry, and record its
// ending vertex and cost from sVertex.
pathData = pq.top(); pq.pop();
ev = pathData.endV;
mincost = pathData.cost;
// Falls der Zielknoten erreicht wurde, wurde der
// kuerzeste Weg vom Start- zum Zielknoten gefunden
if (ev == eVertex)
break;
// Falls der Endknoten schon in l ist, soll er nicht
// weiter betrachtet werden
if (!findVertex(l,ev))
{
// Einfuegen ev in l
l.push_back(ev);
// Bestimme alle Nachbarn des aktuellen Knoten ev, fuer
// jeden Nachbarn der nicht in l ist, erzeuge einen
// Eintrag und fuege ihn ein in die priority queue
// mit Startknoten ev
sv = ev;
adjL = getNachbarn(sv);
// adjLiter durchlaeuft die neue Liste adjL
for(adjLiter = adjL.begin();
adjLiter != adjL.end();
adjLiter++)
{
ev = *adjLiter;
if (!findVertex(l,ev))
{
// Erzeuge neuen Eintrag fuer the priority queue
pathData.startV = sv;
pathData.endV
= ev;
// cost enthalt aktuelle minimale Kosten, hinzu kommen
// die Kosten vom Start- zum Zielknoten
pathData.cost = mincost + getWeight(sv,ev);
pq.push(pathData);
}
221
Die Programmiersprache C++
}
}
}
// Ruechgabe: Erfolg bzw. kein Erfolg
if (ev == eVertex)
return mincost;
else
return -1;
}
Test
Der Aufruf der vorliegenden Methode minimumPath()aus der folgenden main()Routine
int main(int argc, char *argv[])
{
// Knoten des Graphen werden über Grossbuchstaben bezeichnet
Graph<char>
g;
char dName[50];
cout << "Eingabe-Datei: ";
cin >> setw(50) >> dName;
char s;
// Eingabe der Knoten
g.readGraph(dName);
// Prompt fuer den Startknoten
cout << "Berechne den kuerzesten Weg vom Startknoten ";
cin >> s;
vector<char> v = g.getKnoten();
// Kontrolle
vector<char>::iterator vecIter;
for (vecIter = v.begin();vecIter != v.end(); vecIter++)
// cout << " " << *vecIter;
cout << "Kuerzester Weg von " << s << " nach " <<
*vecIter << " ist " <<
g.minimumPath(s,*vecIter) << endl;
system("PAUSE");
return 0;
}
, das Einlesen der Knoten und Kanten von folgenden Graphen
A
752
604
D
B
648
504
432
763
E
355
C
führt zu der folgende Ausgabe:
222
Die Programmiersprache C++
Berechne den kuerzesten Weg
Kuerzester Weg von A nach A
Kuerzester Weg von A nach B
Kuerzester Weg von A nach C
Kuerzester Weg von A nach D
Kuerzester Weg von A nach E
vom
ist
ist
ist
ist
ist
Startknoten A
0
604
648
752
1003
223
Die Programmiersprache C++
4.2 Lineare Listen
4.2.1 Einfach gekettete Listen
Grundlagen
Eine Liste ist verkettet gespeichert, falls jeder Knoten die Adresse der
Arbeitsspeicherzelle enthält, in der sein Nachfolger gespeichert ist.
Zeiger auf den Nachfolger-Knoten
Knoten k
Knoten k‘
Abb.:
Aufbau
Eine lineare Liste kann durch Kombination einer „structure“ mit einem „Zeiger“
erreicht werden. Die „structure“-Variable enthält beliebige Komponenten mit Daten
und einer Komponente, die Zeiger auf die Nachfolger der Listenkomponenten
aufnimmt, z.B.
struct eintrag
{
char name[20];
char vorname[15];
eintrag* zNachf;
};
// Zeiger auf den Nachfolger
Die zusätzliche Komponente zNachf ist eine Zeigervariable, die auf eine Instanz
vom Typ „eintrag“ verweist. Für eine Instanz vom Typ „eintrag“ steht noch kein
Speicherplatz zur Verfügung. Dieser wird zur Programmlaufzeit erzeugt, z.B. mit
eintrag* zelem;
zelem = new eintrag;
Der Inhalt des reservierten Speicherbereichs ist vollkommen undefiniert. Man kann
ihn aber z.B. durch Eingabe über die Tastatur füllen:
cout << "\tName: ";
cin >> zelem->name;
cout << "\tVorname: "; cin >> zelem->vorname;
...............................................
So können alle Komponenten bis auf die letzte gefüllt werden. Zunächst sollte man
immer dafür sorgen, daß der Zeiger nicht irgendwohin, sondern auf eine bestimmte
Speicherstelle zeigt.
Durch
224
Die Programmiersprache C++
zElem->zNachf = NULL;
zeigt die „zNachf“-Komponente auf die symbolische Adresse mit dem Wert 0.
zelem
zAnfang
char name[20];
char vorname[15];
.......
.......
.......
........
eintrag* zelem;
NULL
Abb.: Einfügen des ersten Elements in die linear verkettete Liste (LIFO-Struktur)
Zur Erzeugung eines weiteren Eintrags benötigt man eine weitere Zeigervariable:
eintrag* zAnfang;
Dieser Zeiger erhält den Inhalt von zElem durch zAnfang = zElem;
Auf den zuvor mit new reservierten Speicherbereich zeigen dann zwei Variable. Mit
zElem = new eintrag;
wird ein weiterer Speicherbereich reserviert.. Wieder können die Komponenten
durch Einlesen über die Tastatur mit Werten gefüllt werden. Die letzte Komponente
erhält dieses Mal jedoch nicht Null, sondern die Adresse in zAnfang:
zElem->zNachf = zAnfang;
zelem
zAnfang
char name[20];
char vorname[15];
.......
.......
.......
........
eintrag* zelem;
NULL
........
Abb.: Einfügen des zweiten Elements in die linear verkettete Liste (LIFO-Struktur)
zAnfang wird anschließend auf das neu erzeugte Element gesetzt:
225
Die Programmiersprache C++
zAnfang = zElem;
Die so erzeugte Konstruktion ist eine lineare Liste. Das, was bisher in einzelnen
Anweisungen durchgeführt wurde, läßt sich in einer Schleife zusammenfassen:
do
{
if ((zelem = new eintrag) == NULL)
{
cerr << "\n\tKein Speicherplatz fuer Eintrag vorhanden"; break;
}
cout << "\tName: ";
cin >> zelem->name;
cout << "\tVorname: "; cin >> zelem->vorname;
zElem->zNachf = zAnfang; zAnfang = zElem;
do
{
cout << "\n\tWeiteres Element einfuegen?"; cin >> antwort;
antwort = tolower(antwort);
} while ((antwort != 'j') && (antwort != 'n'));
} while (antwort == 'j');
Nach einigen Schleifendurchläufen ergibt sich folgende Situation:
zelem
zAnfang
char name[20];
char vorname[15];
.......
.......
.......
........
eintrag* zelem;
NULL
........
.....,
.......
Abb.: Linear verkettete Liste (LIFO-Struktur)
Es handelt sich um eine LIFO-Struktur.
Aufgabe zur Listenverarbeitung
1. Mit Hilfe der vorliegenden Darstellung über den Aufbau einer einfach geketteten
Liste soll eine Anwendung „Adreßverwaltung“ entwickelt werden. Die
Adreßverwaltung soll über einen benutzerdefinierten Datentyp „liste“, der eine
einfach gekettete, lineare Liste beschreibt, erfolgen. Zu Beginn der Verarbeitung sind
die Adreßverwaltungsdaten aus einer Textdatei, deren Datensätze (Zeilen)
Adreßdaten enthalten, zu entnehmen bzw. zu sichern148.
#include
#include
#include
#include
148
<iostream.h>
<fstream.h>
<string.h>
<stdlib.h>
vgl. PR43105.CPP
226
Die Programmiersprache C++
typedef int bool;
const int FALSE = 0;
const int TRUE = 1;
struct eintrag
{
char name[20];
char vorname[15];
eintrag* zNachf;
};
class liste
{
private:
eintrag eintr;
eintrag* zAnfang;
char* dateiName;
// Hier werden die Daten abgelegt
public:
liste(char*);
// Konstruktor
~liste(void);
// Destruktor
bool einfuegen(eintrag*);
eintrag* holeEintr(char*);
bool loescheEintr(char*);
};
// Schnittstellenfunktionen
// Konstruktor und Destruktor
liste :: liste(char* dName)
{
// Im Konstruktor werden die Daten aus einer
// externen Datei eingelesen
eintrag* einTr;
fstream datei;
zAnfang
= NULL;
dateiName = dName;
datei.open(dName, ios::in);
// Keine Fehlerbehandlung, falls die Datei nicht existiert,
// da dies beim ersten Start des Programms normal ist
if (datei)
{
// Einlesen der Daten
while (!datei.eof())
{
if ((einTr = new eintrag) == NULL)
{
cerr << "\n\tNicht genuegend Speicher ";
break;
}
datei >> einTr->name;
datei >> einTr->vorname;
// ....................
if (datei.eof()) break;
einTr->zNachf = zAnfang;
zAnfang = einTr;
}
datei.close();
}
}
liste :: ~liste(void)
{
// Im Destruktor wird die Liste wieder in eine externe
// Datei geschrieben
eintrag* zLoe;
227
Die Programmiersprache C++
fstream datei;
datei.open(dateiName, ios::out);
if (!datei)
{
cerr << "\n\tDie Datei " << dateiName
<< " kann nicht zur Ausgabe geoeffnet werden \n";
exit(-1);
}
while (zAnfang != NULL)
{
datei << zAnfang->name << '\n';
datei << zAnfang->vorname << '\n';
// ......
zLoe = zAnfang;
zAnfang = zAnfang->zNachf;
delete zLoe;
// Rueckgabe des Speicherplatzes
}
datei.close();
};
// Transferfunktionen
bool liste :: einfuegen(eintrag* zEintr)
{
/* Ein neuer Listenknoten wird in die Liste eingefuegt
Die Informationen werden an anderer Stelle eingetragen */
if (zEintr != NULL)
{
zEintr->zNachf = zAnfang;
zAnfang = zEintr;
return(TRUE);
}
return(FALSE);
}
eintrag* liste :: holeEintr(char* info)
{
// Suchen eines Eintrags mit der Bezeichnung "info"
eintrag* zElem = zAnfang;
while (zElem != NULL)
{
if (strcmp(info,zElem->name) == 0)
// Eintrag gefunden
return(zElem);
// Kein Eintrag gefunden: Weitersuchen
zElem = zElem->zNachf;
}
return(NULL); // Ende der Liste erreicht
}
bool liste :: loescheEintr(char* info)
{
// Der Eintrag mit dem Namen ifo wird aus der Liste
// entfernt
eintrag* zElem;
eintrag* zLoe;
if (zAnfang == NULL)
// in einer leeren Liste gibt es nichts zu loeschen
return(FALSE);
zElem = zAnfang;
// Weiterer Spezialfall: Das zu loeschende Element ist
// das erste Element in der Liste
if (strcmp(zElem->name,info) == 0)
{
zAnfang = zElem->zNachf;
delete zElem;
228
Die Programmiersprache C++
return(TRUE);
}
while (zElem->zNachf != NULL)
{
if (strcmp(zElem->zNachf->name,info) == 0)
{
zLoe = zElem->zNachf;
zElem->zNachf = zElem->zNachf->zNachf;
delete zLoe;
return(TRUE);
}
zElem = zElem->zNachf;
}
return(FALSE);
}
int main()
{
short menue(void), wahl;
void einfuegenElement(liste&);
void suchenElement(const liste&);
void loeschenElement(liste&);
liste adressen("a:adrlist.dat");
cout << "\n\t A D R E S S E N V E R W A L T U N G";
cout << "\n\t -----------------------------------";
do
{
wahl = menue();
switch(wahl)
{
case 1: einfuegenElement(adressen);
break;
case 2: suchenElement(adressen);
break;
case 3: loeschenElement(adressen);
break;
case 4: break;
}
} while (wahl != 4);
}
short menue(void)
{
short izch;
cout << "\n\tBitte eingeben: ";
cout << "\n\t1: Eingabe eines Datensatzes ";
cout << "\n\t2: Suchen nach einem Datensatz ";
cout << "\n\t3: Loeschen Datensatz aus Liste ";
cout << "\n\t4: Ende Listenbearbeitung ";
cout << "\n\tWahl: ";
do
{
cin >> izch;
} while ((izch < 1) || (izch > 4));
return izch;
}
void einfuegenElement(liste& adressen)
{
eintrag* zelem;
if ((zelem = new eintrag) == NULL)
cerr << "\n\tKein Speicherplatz fuer Eintrag vorhanden";
else
{
229
Die Programmiersprache C++
cout << "\tName: ";
cin >> zelem->name;
cout << "\tVorname: "; cin >> zelem->vorname;
if (adressen.einfuegen(zelem))
cout << "\n\tEintrag konnte eingefuegt werden" << endl;
else
cerr << "\n\tEintrag konnte nicht eingefuegt werden ";
}
}
void suchenElement(const liste& adressen)
{
char suchName[20];
eintrag* zelem;
cout << "\n\tName angeben: ";
cin >> suchName;
if ((zelem = adressen.holeEintr(suchName)) != NULL)
{
cout << "\n\tGefunden:";
cout << "\n\t" << zelem->name << ' ' << zelem->vorname;
}
else cout << "\n\tNicht gefunden";
}
void loeschenElement(liste& adressen)
{
char suchName[20];
cout << "\n\tName angeben: ";
cin >> suchName;
if (adressen.loescheEintr(suchName))
cout << "\n\t" << suchName << " wurde geloescht";
else
cout << "\n\t" << suchName << " konnte nicht geloescht werden";
}
2. Die vorstehende Aufgabe soll mit Hilfe der Klassenschablone „list“ der Standard
Template Library149 gelöst werden.
// ANSI C Headers
#include <stdlib.h>
// C++ STL Headers
#include <algorithm>
#include <iostream>
#include <list>
#include <string>
struct eintrag
{
string name;
string vorname;
eintrag(string n, string v)
{
name = n;
vorname = v;
}
bool operator==(const eintrag& ein)
{
if ((this->name.compare(ein.name) == 0) &&
(this->vorname.compare(ein.vorname) == 0))
return true;
return false;
}
bool operator!=(const eintrag& ein)
149
vgl. 5.2.7
230
Die Programmiersprache C++
{
if ((this->name.compare(ein.name) != 0)
(this->vorname.compare(ein.vorname)
return true;
if ((this->name.compare(ein.name) == 0)
(this->vorname.compare(ein.vorname)
return true;
if ((this->name.compare(ein.name) != 0)
(this->vorname.compare(ein.vorname)
return true;
return false;
&&
!= 0))
&&
!= 0))
&&
== 0))
}
bool operator<(const eintrag& ein)
{
if ((this->name.compare(ein.name) < 0))
return true;
else
return false;
}
friend ostream& operator<<(ostream&, const eintrag&);
};
ostream& operator << (ostream& strm, const eintrag& elem)
{
strm << elem.name << " " << elem.vorname;
return strm;
}
list<eintrag> adressen;
list<eintrag>::iterator iter;
int main()
{
short menue(void), wahl;
void einfuegenElement();
void auflisten();
void suchenElement();
void loeschenElement();
cout << "\n\t A D R E S S E N V E R W A L T U N G";
cout << "\n\t -----------------------------------";
do
{
wahl = menue();
switch(wahl)
{
case 1: einfuegenElement();
break;
case 2: auflisten();
break;
case 3: suchenElement();
break;
case 4: loeschenElement();
break;
case 5: break;
}
} while (wahl != 5);
}
short menue(void)
{
short izch;
cout << "\n\tBitte eingeben: ";
cout << "\n\t1: Eingabe eines Datensatzes ";
cout << "\n\t2: Auflisten der Listenelemente ";
cout << "\n\t3: Suchen nach einem Datensatz ";
231
Die Programmiersprache C++
cout << "\n\t4: Loeschen Datensatz aus Liste ";
cout << "\n\t5: Ende Listenbearbeitung ";
cout << "\n\tWahl: ";
do
{
cin >> izch;
} while ((izch < 1) || (izch > 5));
return izch;
}
void einfuegenElement()
{
string name; string vorname;
cout << "\tName: ";
cin >> name;
cout << "\tVorname: "; cin >> vorname;
eintrag elem(name,vorname);
adressen.push_back(elem);
}
void auflisten()
{
adressen.sort();
cout << "\n\tAuflisten der sortierten Listenelemente:\n";
for ( iter = adressen.begin(); iter != adressen.end(); ++iter )
cout << "\t" << *iter << endl;
}
void suchenElement()
{
string suchName; string suchVorname;
cout << "\n\tName angeben: ";
cin >> suchName;
cout << "\n\tVorname angeben: ";
cin >> suchVorname;
eintrag suchElem(suchName,suchVorname);
iter = find(adressen.begin(),adressen.end(),suchElem);
if (iter != adressen.end())
{
cout << "\n\tGefunden:";
cout << "\n\t" << *iter << endl;
}
else cout << "\n\tNicht gefunden";
}
void loeschenElement()
{
string suchName; string suchVorname;
cout << "\n\tName angeben: ";
cin >> suchName;
cout << "\n\tVorname angeben: ";
cin >> suchVorname;
eintrag suchElem(suchName,suchVorname);
iter = find(adressen.begin(),adressen.end(),suchElem);
if (iter != adressen.end())
{
cout << "\n\tGefunden mit abschliessendem Loeschen:";
cout << "\n\t" << *iter << endl;
list<eintrag>::iterator liter;
liter = iter;
// iter = erase(liter); Fehler !!!
}
else cout << "\n\tNicht gefunden";
}
232
Die Programmiersprache C++
4.2.2 Klassenschablonen für verkettete Listen
4.2.2.1 Doppelt gekettete Listen
vgl. 5.2.3
4.2.2.2 Ringförmig geschlossene Listen
1. Einfach verkettete, ringförmig geschlossene Liste
Aufbau einfach geketteter Ringstrukturen
BASIS
"Leerer Ring"
BASIS
Abb.:
Eine leere ringförmig verkettete Liste enthält eine Listenknoten und ein nicht
initialisiertes Datenfeld. Der Zeiger auf diesen Listenknoten zeigt auf sich selbst. Ein
„Null“-Zeiger existiert in ringförmig verketteten Listen nicht.
Gegeben ist folgende Listenstruktur
ZGR
ZGR1
type Zeiger = record
............
............
Nachf : ^Zeiger;
end;
Abb.:
Eine ringförmige Datenstruktur kann durch die Anweisung
ZGR1^.Nachf := ZGR;
233
Die Programmiersprache C++
erreicht werden. In der Regel zeigt der letzte Knoten in der verkettet gespeicherten
Liste auf den Listenanfang. Ringe können auch folgenden Aufbau besitzen:
Abb.:
Die Klasse „einfach verketteter Ringknoten“ in C++150
// Deklaration Listenknoten
template <class T>
class ringKnoten
{
private:
// ringfoermige Verkettung auf den naechsten Knoten
ringKnoten<T> *nachf;
public:
// "daten" im oeffentlichen Zugriffsbereich
T daten;
// Konstruktoren
ringKnoten(void);
ringKnoten (const T& merkmal);
// Listen-Modifikationsmethoden
void einfuegenDanach(ringKnoten<T> *z);
ringKnoten<T> *loeschenDanach(void);
// beschafft die Adresse des (im Ring) folgenden Knoten
ringKnoten<T> *nachfKnoten(void) const;
};
Die Struktur einer einfach veketteten, ringförmig geschlossenen Liste kann so
dargestellt werden:
daten:
nachf:
Abb.: leere Liste
daten:
nachf:
Abb.: Liste mit Knoten
// Schnittstellenfunktionen
150
vgl. ringkno.h
234
Die Programmiersprache C++
Der Konstruktor initialisiert einen Knoten, der einen Zeiger enthält, der auf diesen
Knoten zurück verweist. So kann jeder Knoten den Anfang einer leeren Liste
repräsentieren. Das Datenfeld des Knoten bleibt in diesem Fall unbesetzt.
// Konstruktor der eine Liste deklariert und "daten"
// uninitialisiert laesst.
template <class T>
ringKnoten<T>::ringKnoten(void)
{
// initialisiere den Knoten, so dass er auf sich selbst zeigt
nachf = this;
}
// Konstruktor der eine leere Liste erzeugt und "daten"
// initialisiert
template <class T>
ringKnoten<T>::ringKnoten(const T& merkmal)
{
// setze den Knoten so, dass er auf sich selbst zeigt
// und initialisiere "daten"
nachf = this;
daten = merkmal;
}
Die Methode nachfKnoten() ermittelt einen Verweis auf den nächsten in der
einfach verketteten, ringförmig geschlossenen Liste. Die Methode soll das
Durchlaufen der Liste erleichtern.
// Rueckgabe des Zeiger auf den naechsten Knoten
template <class T>
ringKnoten<T> *ringKnoten<T>::nachfKnoten(void) const
{
return nachf;
}
Die Methoden zur Modifikation der Liste einfuegenDanach(ringKnoten<T>
*z); fügt die Listenknoten unmittelbar nach dem Anfangsknoten (der die leere Liste
definiert) ein.
vor dem Einfügen:
nach dem Einfügen:
daten:
nachf:
z
Abb.: Einfügen des Knoten „z“ in eine leere Liste
vor dem Einfügen:
nach dem Einfügen:
daten:
nachf:
Z
Abb.: Einfügen des Knoten „z“ in ein einfach gekettete, ringförmig geschlossene Liste mit Listenknoten
// Einfuegen eines Knoten z nach dem aktuellen Knoten
235
Die Programmiersprache C++
template <class T>
void ringKnoten<T>::einfuegenDanach(ringKnoten<T> *z)
{
// z zeigt auf den Nachfolger des aktuellen Knoten,
// der aktuellen Knoten zeigt auf z
z->nachf = nachf;
nachf = z;
}
Die Methode loeschenDanach() löscht den Listenknoten unmittelbar nach dem
aktuellen Knoten.
// Loesche den Knoten, der dem aktuellen Knoten folgt und gib seine
// Adresse zurueck
template <class T>
ringKnoten<T> *ringKnoten<T>::loeschenDanach(void)
{
// Sichere die Adresse des Knoten, der geloescht werden soll
ringKnoten<T> *tempZgr = nachf;
// Falls "nachf" mit der Adresse des aktuellen Objekts (this) ueberein// stimmt, wird auf sich selbst gezeigt. Hier darf nicht geloescht werden
// (Rueckgabewert NULL)
if (nachf == this) return NULL;
// Der aktuelle Knoten zeigt auf denNachfolger von tempZgr.
nachf = tempZgr->nachf;
// Gib den Zeiger auf den ausgeketteten Knoten zurueck
return tempZgr;
}
Anwendung: Das „Josephus-Problem“
Aufgabenstellung: Ein Reisebüro verlost eine Weltreise unter „N“ Kunden. Dazu
werden die Kunden von 1 bis N durchnummeriert, eine Bediensteter des Reisebüros
hat in einem Hut N Lose untergebracht. Ein Los wird aus dem Hut gezogen, es hat
die Nummer M (1 <= M <= N). Zur Auswahl des glücklichen Kunden stellt man sich
dann folgendes vor: Die Kunden (identifiziert durch die Nummern 1 bis N) werden in
einem Kreis angeordnet und mit Hilfe der gezogenen Losnummer aus diesem Kreis
entfernt. Bei bspw. 8 Kunden und der gezogenen Losnummer 3 werden, da das
Abzählen bzw. Entfernen im Uhrzeigersinn erfolgt, folgende Nummern aus dem
Kreis entfernt: 3, 6, 1, 5, 2, 8, 4. Die Person 7 gewinnt die Reise.
Lösung151:
#include <iostream.h>
#include <stdlib.h>
#include "ringkno.h"
// Erzeuge eine ringfoermig verkettete Liste mit gegebenem Anfang
void erzeugeListe(ringKnoten<int> *anfang, int n)
{
// Beginn des Einfuegevorgangs
ringKnoten<int> *aktZgr = anfang, *neuerKnotenWert;
int i;
// Erzeuge die n Elemente umfassende ringfoermige Liste
for(i=1;i <= n;i++)
{
// Belege den Knoten mit Datenwert
neuerKnotenWert = new ringKnoten<int>(i);
// Einfuegen am Listenende
151
PR22221.CPP
236
Die Programmiersprache C++
aktZgr->einfuegenDanach(neuerKnotenWert);
aktZgr = neuerKnotenWert;
}
}
// Gegeben ist eine n Elemente umfassende, ringfoermige Liste; loese das
// Josephus-Problem durch Loeschen jeder m. Person bis nur
// eine Person uebrig bleibt
void Josephus(ringKnoten<int> *liste, int n, int m)
{
// vorgZgr bewegt aktZgr durch die Liste
ringKnoten<int> *vorgZgr = liste, *aktZgr = liste->nachfKnoten();
ringKnoten<int> *geloeschterKnotenZgr;
// Loesche alle bis auf eine Person aus der Liste
for(int i=0;i < n-1;i++)
{
// Zaehle die Personen jeweils an der aktuelle Stelle
// Suche m Personen auf
for(int j=0;j < m-1;j++)
{
// Ausrichten der Zeiger
vorgZgr = aktZgr;
aktZgr = aktZgr->nachfKnoten();
// Falls "aktZgr am Anfang steht, bewege die Zeiger weiter
if (aktZgr == liste)
{
vorgZgr = liste;
aktZgr = aktZgr->nachfKnoten();
}
}
cout << "Loesche Person " << aktZgr->daten << endl;
// Ermittle den zu loeschenden Knoten und aktualisiere aktZgr
geloeschterKnotenZgr = aktZgr;
aktZgr = aktZgr->nachfKnoten();
// loesche den Knoten aus der Liste
vorgZgr->loeschenDanach();
delete geloeschterKnotenZgr;
// Falls aktZgr am Anfang steht, bewege Zeiger weiter
if (aktZgr == liste)
{
vorgZgr = liste;
aktZgr = aktZgr->nachfKnoten();
}
}
cout << endl << "Ausgezaehlt wurde " << aktZgr->daten << endl;
// Loesche den uebrig gebliebenen Knoten
geloeschterKnotenZgr = liste->loeschenDanach();
delete geloeschterKnotenZgr;
}
void main(void)
{
// Liste mit Personen
ringKnoten<int> liste;
// n ist die Anzahl der Personen, m ist die Abzaehlgroesse
int n, m;
cout << "Anzahl Bewerber? ";
cin >> n;
// Erzeuge eine ringfoermig gekettete Liste mit Personen 1, 2, ... n
erzeugeListe(&liste,n);
// Zufallswert: 1 <= m <= n
randomize();
m = 1 + random(n);
cout << "Erzeugte Zufallszahl " << m << endl;
// loese das Josephus Problem und gib den Gewinner aus
Josephus(&liste,n,m);
}
/*
<Ablauf des Programms>
237
Die Programmiersprache C++
Anzahl der Bewerber? 10
Erzeugte Zufallszahl 5
Loesche Person 5
Loesche Person 10
Loesche Person 6
Loesche Person 2
Loesche Person 9
Loesche Person 8
Loesche Person 1
Loesche Person 4
Loesche Person 7
Person 3 gewinnt.
*/
2. Doppelt verkettete, ringförmig geschlossene Liste
Basis
Abb.: Doppelt gekettete Ringstruktur
Leerer Ring
Basis
Abb.: Der leere Ring in einer doppelt geketteten Ringstruktur
Doppelt verkettete Listen erweitern den durch ringförmig verkettete Listen
bereitgestellten Leistungsumfang beträchtlich. Sie erleichtern das Einfügen und das
Löschen durch Zugriffsmöglichkeinten in zwei Richtungen:
238
Die Programmiersprache C++
links
daten
rechts
......
.....
4
1
2
3
Abb.:
Klassenschablone „doppelt verketteter RingKnoten“152
template <class T> class dkringKnoten
{
private:
// ringfoermig angeornete Verweise nach links und rechts
dkringKnoten<T> *links;
dkringKnoten<T> *rechts;
public:
// daten steht unter oeffentlichem Zugriff
T daten;
// Konstruktoren:
dkringKnoten(void);
dkringKnoten (const T& merkmal);
// Modifikation der Listen
void einfuegenRechts(dkringKnoten<T> *z);
void einfuegenLinks(dkringKnoten<T> *z);
dkringKnoten<T> *loescheKnoten(void);
// Beschaffen der Adressen der nachfolgenden Knoten auf der
// linken und rechten Seite
dkringKnoten<T> *nachfKnotenRechts(void) const;
dkringKnoten<T> *nachfKnotenLinks(void) const;
};
Methoden für doppelt verketteten Listenknoten einer ringförmig geschlossenen Liste
Konstruktoren
// Konstruktor: erzeugt eine leere Liste, das Datenfeld bleibt
// ohne Initialisierung; wird zur Definition des Listenanfangs benutzt
template <class T>
dkringKnoten<T>::dkringKnoten(void)
{ // ínitialisiert den Knoten mit einem Zeiger, der auf den
// Knoten zeigt
links = rechts = this;
}
// Konstruktor: erzeugt eine leere Liste und intialisierte das Feld daten
template <class T>
dkringKnoten<T>::dkringKnoten(const T& merkmal)
{ // initialisiert den Knoten mit einem Zeiger der
// auf den Knoten zeigt und initialisiert das Datenfeld
links = rechts = this;
daten = merkmal;
}
Einfügen eines Knoten
152
dringkn.h
239
Die Programmiersprache C++
// Fuege einen Knoten z rechts zum aktuellen Knoten ein
template <class T>
void dkringKnoten<T>::einfuegenRechts(dkringKnoten<T> *z)
{ // kette z zu seinem Nachfolger auf der rechten Seite ein
z->rechts = rechts;
rechts->links = z;
// verkette z mit dem aktuellen Knoten auf seiner linkten Seite
z->links = this;
rechts = z;
}
// Fuege einen Knoten z links zum aktuellen Knoten ein
template <class T>
void dkringKnoten<T>::einfuegenLinks(dkringKnoten<T> *z)
{ // kette z zu seinem Nachfolger auf der linken Seite ein
z->links = links;
links->rechts = z;
// verkette z mit dem aktuellen Knoten auf seiner rechten Seite
z->rechts = this;
links = z;
}
Löschen
// Ausketten des aktuellen Knoten aus der Liste
template <class T>
dkringKnoten<T> *dkringKnoten<T>::loescheKnoten(void)
{
// Knotenverweis "links" muss verkettet werden mit dem
// Verweis des aktuellen Knoten nach rechts
links->rechts = rechts;
// Knotenverweis "rechts" muss verkettetet werden mit dem
// Verweis des aktuellen Knoten nach links
rechts->links = links;
// Rueckgabe der Adresse vom aktuellen Knoten
return this;
}
Bestimmen der nachfolgenden Knoten
// Rueckgabe Zeiger zum naechsten Knoten auf der rechten Seite
template <class T>
dkringKnoten<T> *dkringKnoten<T>::nachfKnotenRechts(void) const
{
return rechts;
}
// Rueckgabe Zeiger zum naechsten Knoten auf der linken Seite
// return pointer to the next node on the left
template <class T>
dkringKnoten<T> *dkringKnoten<T>::nachfKnotenLinks(void) const
{
return links;
}
Anwendung: Einfügen eines doppelt verketteten Listenknoten in eine geordnete Fole
von Listenknoten153
Falls der Aufbau einer geordneten Folge von doppelt verketteten Listenknoten im
Rahmen einer ringförmig verketteten Liste gelingt, kann die Liste in Vorwärtsrichtung
153
PR22225.CPP
240
Die Programmiersprache C++
(links) durchlaufen bzgl. der in den Listenknoten gespeicherten Daten eine
aufsteigende Sortierung zeigen und ,in Rückwärtsrichtung (rechts) durchwandert,
eine absteigende Sortierung aufweisen. Mit zwei Funktionsschablonen
einfuegenKleiner() und einfuegenGroesser() soll dies erreicht werden.
Zum Aufbau der ringförmig, doppelt verketteten Liste wird die Funktionsschablone
DverkSort() herangezogen, die zum geordneten Einfügen die Funktionsschablone
einfuegenKleiner() und einfuegenGroesser() benutzt und den
Anfangszeiger „dkAnfang“ verwaltet.
template <class T>
void einfuegenKleiner(dkringKnoten<T> *dkAnfang,
dkringKnoten<T>* &aktZgr, T merkmal)
{
dkringKnoten<T> *neuerKnoten= new dkringKnoten<T>(merkmal), *z;
// Bestimme den Einfuegepunkt
z = aktZgr;
while (z != dkAnfang && merkmal < z->daten) z = z->nachfKnotenLinks();
// Einfuegen des Knotens mit dem Datenelement
z->einfuegenRechts(neuerKnoten);
// Ruecksetzen aktZgr auf den neuen Knoten
aktZgr = neuerKnoten;
}
template <class T>
void einfuegenGroesser(dkringKnoten<T>* dkAnfang,
dkringKnoten<T>* & aktZgr, T merkmal)
{
dkringKnoten<T> *neuerKnoten= new dkringKnoten<T>(merkmal), *z;
// Bestimmen des Einfuegepunkts
z = aktZgr;
while (z != dkAnfang && z->daten < merkmal) z = z->nachfKnotenRechts();
// Einfuegen des Datenelements
z->einfuegenLinks(neuerKnoten);
// Ruecksetzen des aktuellen Zeigers auf neuerKnoten
aktZgr = neuerKnoten;
}
template <class T>
void DverkSort(T a[], int n)
{ // Die doppelt verkettete Liste soll Feld-Komponenten aufnehmen
dkringKnoten<T> dkAnfang, *aktZgr;
int i;
// Einfuegen des ersten Elements in die doppelt verkettete Liste
dkringKnoten<T> *neuerKnoten = new dkringKnoten<T>(a[0]);
dkAnfang.einfuegenRechts(neuerKnoten);
aktZgr = neuerKnoten;
// Einbrigen weiterer Elemente in die doppelt verkettete Liste
for (i = 1; i < n; i++)
if (a[i] < aktZgr->daten) einfuegenKleiner(&dkAnfang,aktZgr,a[i]);
else einfuegenGroesser(&dkAnfang,aktZgr,a[i]);
// Durchlaufe die Liste und kopiere die Datenwerte zurueck in den "array"
aktZgr = dkAnfang.nachfKnotenRechts();
i = 0;
while(aktZgr != &dkAnfang)
{
a[i++] = aktZgr->daten;
aktZgr = aktZgr->nachfKnotenRechts();
}
// Loesche alle Knoten in der Liste
while(dkAnfang.nachfKnotenRechts() != &dkAnfang)
{
aktZgr = (dkAnfang.nachfKnotenRechts())->loescheKnoten();
delete aktZgr;
}
241
Die Programmiersprache C++
}
Der folgende Hauptprogrammabschnitt ruft die vorliegende Funktionsschablone zum
Sortieren eines Arbeitsspeicherfelds auf
void main(void)
{ // Ein initialisierter "array" mit 10 Ganzzahlen
int A[10] = {82,65,74,95,60,28,5,3,33,55};
DverkSort(A,10);
// sortiere "array"
cout << "Sortiertes Feld:
";
for(int i=0;i < 10;i++) cout << A[i] << " "; cout << endl;
}
242
Die Programmiersprache C++
4.3 Tabellen
4.3.1 Einfache und Sortierte Tabellen 154
vgl. 5.2.4
4.3.2 Hash-Tabellen
Grundlage: Hash-Funktion
Eine grundliegende Idee, das Suchen nach bestimmten Tabelleneinträgen zu
beschleunigen, ist: Aus dem Suchbegriff ist durch einen Umrechnungsalgorithmus
der Index zu berechnen, die direkt auf den Tabelleneintrag führt, der den Suchbegriff
enthält. Allgemein wird ein derartiger Algorithmus „Hashing“ genannt. Die „Hashing“Funktion ist (nach der Divisions-Rest-Methode) bestimmt durch:
H(Schl) = SCHL mod M
„M“ ist die Größe der Tabelle, der Suchbegriff wird Schlüssel (SCHL) genannt. Die
gleiche Methode dient auch zum Speichern der Tabelleneinträge. Kollisionen sind
dabei möglich, in solchen Fällen bestimmt die Hash-Funktion trotz unterschiedlicher
Schlüssel jeweils den gleichen (Index-) Wert.
Auflösen von Kollisionen
Eine Hash-Funktion, die keine Kollisionen verursacht, heißt perfekt. Parktisch kann
perfektes Hashing nur für eine fest vorgegebene Anzahl von Schlüsseln erreicht
werden. Generell führen Hash-Funktionen zu Kollisionen. Zum Auflösen von
Kollisionen gibt es zwei unterschiedliche Vorgehensweisen:
- Die kollidierenden Schlüssel werden aneinander verkettet (chaining).
- Von einer Anfangsadresse (Anfangsindex) wird eine Folge weiterer Adressen
(Indexe) durchlaufen (open adressing)
1. Kettungstechniken: seperate chaining
Alle Schlüssel, die sich auf denselben Ort abbilden, werden mit Hilfe einer geketteten
Liste verwaltet. Die Hash-Tabelle enthält dann nur noch Zeiger auf Listenknoten155.
Bsp.: Erstellen und Verwalten eines Telefon-Verzeichnisses mit einer Hash-Tabelle
im Rahmen des „seperate chaining“. Das Verzeichnis ist bisher in einer
Textdatei enthalten. Die in dieser Tabelle gespeicherten Sätze haben folgenden
Aufbau:
154
155
PR44205.CPP
PR44310.CPP
243
Die Programmiersprache C++
Name
Juergen
......................
Telefon-Nr.
93622
.........
Die Datensätze dieser Datei sollen in eine Hash-Tabelle übernommen werden und
dort verwaltet werden (Einfügen, Löschen, Wiederauffinden von Datensätzen). Der
Hashtabellen-Zugriff soll über den Namen im Datensatz erfolgen. Der Aufbau der
Hash-Tabelle und die dort eingetragenen Verweise sollen nach dem Prinzip des
„seperate chaining“ organisiert werden.
~
~
~
~
Hashtabelle
Abb.: Hash-Tabelle für „seperate chaining“
Die von der Hash-Tabelle ausgehenden Verweise bilden eine lineare Liste. Eine
solche Liste wird zweckmäßig über einen benutzerdefinierten Datentyp „liste“ zum
Erzeugen und Verwalten einer einfachen, geketteten Liste realisiert.
2. Überlaufverfahren ohne Kettung: open adressing
Diese Verfahren suchen bei Adreßkollisionen nach einem freien Tabellenplatz. Bei
der Bestimmung solcher Folgen von Adressen (Indexfolgen) sollen möglichst wenig
Überlappungen (Häufungen) entstehen. Zur Bildung von Folgen (für Adressen,
Indexe) haben sich einige typische Methoden durchgesetzt:
a) Lineare Fortschaltung
Liefert die Namens-Adreßtransformation eine Adresse „as“ und der schon ein Name
eingetragen ist, dan wird durch lineares Fortschalten as+1, as+2, as+3, ... der
nächste freie Platz in der Hash-Tabelle gesucht. Dort wird dann das Schlüsselwort
eingetragen. Ist as + i > M, dann wird die Tabelle zyklisch von vorn durchlaufen
b) Zufälliges Suchen
Im Kollisionsfall wird mit Hilfe einer Zufallszahl ein Ersatzplatz gesucht:
1) Berechnung einer Tabellenadresse as (Nach einer der beschriebenen Transformationen)
244
Die Programmiersprache C++
2) Vergleich des vorgegebenen Schlüssels mit dem unter as eingetragenen Namen. Im Kollisionsfall
weiter bei (3), andernfalls STOP.
3) Berechnung einer Zufallszahl xi und Bildung einer Zuordnung as = as + xi mod M
c) Double Hashing
Hier werden zwei voneinander unabhängige Hash-Funktionen benutzt. Die erste
Funktion dient zur Ermittlung der Postion. Die zweite bestimmt, falls die Position
belegt ist, die nächste freie Position im Rahmen des „open adressing“ 156:
d) quadratisches Sondieren
Zuerst wird im Kollisionsfall die unmittelbar folgende Position in der Hashtabelle
untersucht (as + 1). Danach wird, falls die Position besetzt ist, der Index um 4
Einheiten heraufgesetzt (as + 4). Führt das auch nicht zum Erfolg, dann wird der
Index um 9 Einheiten erhöht (as + 9). Die zur Erhöhung der Position dienenden
Quadratzahlen kann man über eine einfache Addition bestimmen:
0 1 4 9 16 25 ..
1 3 5 7 11 ..
Man braucht also nur die Zahl, die zur jeweils vorletzten Quadratzahl addiert wurde,
eine 2 hinzuzufügen, und man erhält die neue Quadratzahl.
Mit dem quadratischen Sondieren erreicht man mindestens die Hälfte aller
Positionen, falls die Tabellengröße eine Primzahl ist.
156
PR44315.CPP
245
Die Programmiersprache C++
4.4 Binärbäume
Freie binäre IntervallBäume
Aufbau
Der Binärbaum ist eine Datenstruktur, in der jedes Element zwei Zeiger besitzt und
daher maximal zwei Nachfolger besitzen kann.
Der Einstiegpunkt in den binären Baum heißt Wurzel. Von der Wurzel verzweigt sich
der binäre Baum nach unten. Die Elemente eines binären Suchbaums werden bei
der Verzweigung nach einer ganz bestimmten Reihenfolge abgespeichert. Ein neues
Element wird eingefügt, indem es nach einem, im Element gespeicherten Kriterium
mit der Wurzel verglichen wird. Liefert der Vergleich „kleiner“ wird das neue Element
dem linken Nachfolger zugeordnet, im anderen Fall wird zum rechten Nachfolger
verzweigt. Der Vergleich wird solange vorgenommen, bis keine weiteren Nachfolger
mehr vorhanden sind.
wurzel
Abb. 4.5-1: Ein ausgeglichener Binärbaum
Ordnungsrelation und Darstellung
Freie Bäume sind durch folgende Ordnungsrelation bestimmt:
In jedem Knoten eines knotenorientierten, geordneten Binärbaums gilt: Alle
Schlüssel im rechten (linken) Unterbaum sind größer (kleiner) als der Schlüssel im
Knoten selbst.
Mit Hilfe dieser Ordnungsrelation erstellte Bäume dienen zum Zugriff auf
Datenbestände (Aufsuchen eines Datenelements). Die Daten sind die Knoten
(Datensätze, -segmente, -elemente). Die Kanten des Zugriffsbaums sind Zeiger auf
weitere Datenknoten (Nachfolger).
246
Die Programmiersprache C++
Dateninformation
Knotenzeiger
Schluessel
Datenteil
Links
Rechts
Zeiger
Zeiger
zum linken
zum rechten
Nachfolgeknoten
Abb. 4.5-2: Knoten eines binären Suchbaums
Die Klassenschablone BaumKnoten
// Schnittstellenbeschreibung
// Die Klasse binaerer Suchbaum binSBaum benutzt die Klasse baumKnoten
template <class T> class binSBaum;
// Deklaration eines Binaerbaumknotens fuer einen binaeren Baum
template <class T> class baumKnoten
{
protected:
// zeigt auf die linken und rechten Nachfolger des Knoten
baumKnoten<T> *links;
baumKnoten<T> *rechts;
public:
// Das oeffentlich zugaenglich Datenelement "daten"
T daten;
// Konstruktor
baumKnoten (const T& merkmal, baumKnoten<T> *lzgr = NULL,
baumKnoten<T> *rzgr = NULL);
// virtueller Destruktor
virtual ~baumKnoten(void);
// Zugriffsmethoden auf Zeigerfelder
baumKnoten<T>* holeLinks(void) const;
baumKnoten<T>* holeRechts(void) const;
// Die Klasse binSBaum benoetigt den Zugriff auf
// "links" und "rechts"
friend class binSBaum<T>;
};
Die Dateninformation des Binärbaumknotens kann folgendermaßen beschrieben
werden:
struct info
{
// int schl;
char* schl;
char* vorname;
};
// Schluessel
// irgendwelche Daten
// Schnittstellenfunktionen
// Konstruktor: Initialisiert "daten" und die Zeigerfelder
// Der Zeiger NULL verweist auf einen leeren Baum
template <class T>
baumKnoten<T>::baumKnoten (const T& merkmal, baumKnoten<T> *lzgr,
baumKnoten<T> *rzgr): daten(merkmal), links(lzgr), rechts(rzgr) {}
// Die Methode holeLinks ermoeglicht den Zugriff auf den linken
247
Die Programmiersprache C++
// Nachfolger
template <class T>
baumKnoten<T>* baumKnoten<T>::holeLinks(void) const
{
// Rueckgabe des Werts vom privaten Datenelement links
return links;
}
// Die Methode "holeRechts" erlaubt dem Benutzer den Zugriff auf den
// rechten Nachfoger
template <class T>
baumKnoten<T>* baumKnoten<T>::holeRechts(void) const
{
// Rueckgabe des Werts vom privaten Datenelement rechts
return rechts;
}
// Destruktor: tut eigentlich nichts
template <class T>
baumKnoten<T>::~baumKnoten(void)
{}
Aufsuchen von in Baumknoten gespeicherten Schlüsselwerten
Ausganspunkt ist der Wurzelknoten:
baumKnoten<T>* wurzel;
// Zeiger auf den Wurzelknoten
Das Aufsuchen eines Elements im Zugriffsbaum geht vom Wurzelknoten über einen
Kantenzug (d.i. eine Reihe von Zwischenknoten) zum gesuchten Datenelement. Bei
jedem Zwischenknoten auf diesem Kantenzug findet ein Entscheidungsprozeß über
die folgenden Vergleiche statt:
1. Die beiden Schlüssel sind gleich: Das Element ist damit gefunden
2. Der gesuchte Schlüssel ist kleiner: Das gesuchte Element kann sich dann nur im linken
Unterbaum befinden
3. Der gesuchte Schlüssel ist größer: Das gesuchte Element kann sich nur im rechten Unterbaum
befinden.
Das Verfahren wird solange wiederholt, bis das gesuchte (Schlüssel-) Element
gefunden ist bzw. feststeht, daß es in dem vorliegenden Datenbestand nicht
vorhanden ist.
Struktur und Wachstum binärer Bäume sind durch die Ordnungrelation bestimmt.
Aufgabe: Betrachte die 3 Schlüssel 1, 2, 3. Diese 3 Schlüssel können durch
verschieden angeordnete Folgen bei der Eingabe unterschiedliche binäre
Bäume erzeugen. Zeichne alle Bäume, die aus unterschiedlichen
Eingaben der 3 Schlüssel resultieren, auf.
248
Die Programmiersprache C++
1, 2, 3
1, 3, 2
2, 1, 3
1
2
1
2
3
3
1
1
3
2
2, 3, 1
3, 1, 2
3, 2, 1
2
3
3
3
1
2
2
1
Abb. 4.5-3: 6 unterschiedliche Eingabefolgen bewirken 6 unterschiedliche Bäume
Es gibt also: 6 unterschiedliche Eingabefolgen und somit 6 unterschiedliche Bäume.
Allgemein können n Elemente zu n! verschiedenen Anordnungen zusammengestellt
werden.
Zur Realisierung der Ordnungsrelation werden, je nach unterschiedlichem Datentyp
der Schlüssel, Vergleichsfunktionen benötigt.
Ohne Berücksichtigung der Ordnungsrelation kann ein binärer Baum von folgender
Gestalt aus Elementen der Klassenschablone „baumknoten“ zusammengestellt
werden:
‘A’
‘B’
‘C’
‘D’
‘E’
int main()
{
baumKnoten<char> *a, *b, *c, *d, *e;
d = new baumKnoten<char>('D');
e = new baumKnoten<char>('E');
b = new baumKnoten<char>('B',NULL,d);
c = new baumKnoten<char>('C',e);
a = new baumKnoten<char>('A',b,c);
wurzel = a;
}
249
Die Programmiersprache C++
Funktionsschablonen zur Bearbeitung binärer Bäume
a) Ausgabe eines binären Baums
Die Ausgabe soll auf das Standardausgabegerät erfolgen
// Ausgabefunktionen fuer die Darstellung des Binaerbaums
// Zwischenraum zwischen den Stufen
const int anzZwischenraum = 6;
// Einfuegen einer anzahl Zwischenraumzeichen auf der
// aktuellen Zeile
void ausgAnzZwischenraum(int num)
{
for (int i = 0; i < num; i++)
cout << " ";
}
template <class T>
void ausgBaum(baumKnoten<T> *b, int stufe)
{
if (b != NULL)
{
// gib den rechten Teilbaum aus
ausgBaum(b->holeRechts(), stufe + 1);
ausgAnzZwischenraum(anzZwischenraum * stufe);
cout << b->daten << endl;
ausgBaum(b->holeLinks(),stufe + 1);
}
}
b) Zählen der Blätter
// Anzahl Blätter
template <class T>
void anzBlaetter(baumKnoten<T>* b, int& zaehler)
{
// benutze den Postorder-Durchlauf
if (b != NULL)
{
anzBlaetter(b->holeLinks(), zaehler);
anzBlaetter(b->holeRechts(), zaehler);
// Pruefe, ob der erreichte Knoten ein Blatt ist
if (b->holeLinks() == NULL && b->holeRechts() == NULL)
zaehler++;
}
}
c) Ermitteln der Tiefe bzw. der Höhe
// Hoehe des Baums
template <class T>
int hoehe(baumKnoten<T>* b)
{
int hoeheLinks, hoeheRechts, hoeheWert;
if (b == NULL)
hoeheWert = -1;
else
{
hoeheLinks = hoehe(b->holeLinks());
hoeheRechts = hoehe(b->holeRechts());
hoeheWert = 1 +
(hoeheLinks > hoeheRechts ? hoeheLinks : hoeheRechts);
}
250
Die Programmiersprache C++
return hoeheWert;
}
d)Kopieren des Baums
// Kopieren eines Baums
template <class T>
baumKnoten<T>* kopiereBaum(baumKnoten<T>* b)
{
baumKnoten<T> *neuerLzgr, *neuerRzgr, *neuerKnoten;
// Rekursionsendebedingung
if (b == NULL)
return NULL;
if (b->holeLinks() != NULL)
neuerLzgr = kopiereBaum(b->holeLinks());
else
neuerLzgr = NULL;
if (b->holeRechts() != NULL)
neuerRzgr = kopiereBaum(b->holeRechts());
else neuerRzgr = NULL;
// Der neue Baum wird von unten her aufgebaut,
// zuerst werden die Nachfolger bearbeitet und
// dann erst der Vaterknoten
neuerKnoten = erzeugebaumKnoten(b->daten, neuerLzgr, neuerRzgr);
// Rueckgabe des Zeigers auf den zuletzt erzeugten Baumknoten
return neuerKnoten;
}
f) Erzeugen bzw. Freigabe eines Binärbaumknotens
template <class T>
baumKnoten<T>* erzeugebaumKnoten(T merkmal,
baumKnoten<T>* lzgr = NULL,
baumKnoten<T>* rzgr = NULL)
{
baumKnoten<T> *z;
// Erzeugen eines neuen Knoten
z = new baumKnoten<T>(merkmal,lzgr,rzgr);
if (z == NULL)
{
cerr << "Kein Speicherplatz!\n";
exit(1);
}
return z; // Rueckgabe des Zeigers
}
Der durch den Baumknoten belegte
Funktionsschablone freigegeben werden:
template <class T>
void gibKnotenFrei(baumKnoten<T>* z)
{
delete z;
}
g) Löschen des Baums
// Loeschen des Baums
template <class T>
void loescheBaum(baumKnoten<T>* b)
{
if (b != NULL)
{
251
Speicherplatz
kann
über
folgende
Die Programmiersprache C++
loescheBaum(b->holeLinks());
loescheBaum(b->holeRechts());
gibKnotenFrei(b);
}
}
Die in den Funktionsschablonen angesprochenen Verarbeitungsfunktionen zum
Bestimmen eines Baumknotens anhand des Schlüsselwerts (Merkmal), zum
Löschen eines Baums, zum Kopieren eines binären Baums, zur
Speicherbeschaffung bzw. Speicherfreigabe für Baumknoten bilden die privaten
(internen) Verarbeitungsmethoden einer Klassenschablone für einen binären
Suchbaum.
Schnittstellenbeschreibung (Deklaration) einer Klassenschablone für den binären
Suchbaum:
#include "baumkno.h"
template <class T> class binSBaum
{
protected:
// Zeiger auf den Wurzelknoten und den Knoten, auf den am
// haeufigsten zugegriffen wird
baumKnoten<T> *wurzel;
baumKnoten<T> *aktuell;
// Anzahl Knoten im Baum
int groesse;
// Speicherzuweisung / Speicherfreigabe
// Zuweisen eines neuen Baumknoten mit Rueckgabe
// des zugehoerigen Zeigerwerts
baumKnoten<T> *holeBaumKnoten(const T& merkmal,
baumKnoten<T> *lzgr,baumKnoten<T> *rzgr)
{
baumKnoten<T> *z;
// Datenfeld ubd die beiden Zeiger werden initialisiert
z = new baumKnoten<T> (merkmal, lzgr, rzgr);
if (z == NULL)
{
cerr << "Speicherbelegungsfehler!\n";
exit(1);
}
return z;
}
// gib den Speicherplatz frei, der von einem Baumknoten belegt wird
void freigabeKnoten(baumKnoten<T> *z)
// wird vom Kopierkonstruktor und Zuweisungsoperator benutzt
{ delete z; }
// Kopiere Baum b und speichere ihn im aktuellen Objekt ab
baumKnoten<T> *kopiereBaum(baumKnoten<T> *b)
// wird vom Destruktor, Zuweisungsoperator und bereinigeListe benutzt
{
baumKnoten<T> *neulzgr, *neurzgr, *neuerKnoten;
// Falls der Baum leer ist, Rueckgabe von NULL
if (b == NULL) return NULL;
// Kopiere den linken Zweig von der Baumwurzel b und weise seine
// Wurzel neulzgr zu
if (b->links != NULL) neulzgr = kopiereBaum(b->links);
else neulzgr = NULL;
// Kopiere den rechten Zweig von der Baumwurzel b und weise seine
// Wurzel neurzgr zu
if (b->rechts != NULL) neurzgr = kopiereBaum(b->rechts);
else neurzgr = NULL;
// Weise Speicherplatz fuer den aktuellen Knoten zu und weise seinen
// Datenelementen Wert und Zeiger seiner Teilbaeume zu
252
Die Programmiersprache C++
neuerKnoten = holeBaumKnoten(b->daten, neulzgr, neurzgr);
return neuerKnoten;
}
// Loesche den Baum, der durch im aktuellen Objekt gespeichert ist
void loescheBaum(baumKnoten<T> *b)
// Lokalisiere einen Knoten mit dem Datenelementwert von merkmal
// und seinen Vorgaenger (eltern) im Baum
{
// falls der aktuelle Wurzelknoten nicht NULL ist, loesche seinen
// linken Teilbaum, seinen rechten Teilbaum und dann den Knoten selbst
if (b != NULL)
{
loescheBaum(b->links);
loescheBaum(b->rechts);
freigabeKnoten(b);
}
}
// Suche nach dem Datum "merkmal" im Baum. Falls gefunden, Rueckgabe
// der zugehoerigen Knotenadresse; andernfalls NULL
baumKnoten<T> *findeKnoten(const T& merkmal,
baumKnoten<T>* & eltern) const
{ // Durchlaufe b. Startpunkt ist die Wurzel
baumKnoten<T> *b = wurzel;
// Die "eltern" der Wurzel sind NULL
eltern = NULL;
// Terminiere bei einen leeren Teilbaum
while(b != NULL)
{
// Halt, wenn es passt
if (merkmal == b->daten) break;
else
{ // aktualisiere den "eltern"-Zeiger und gehe nach rechts bzw. nach
// links
eltern = b;
if (merkmal < b->daten) b = b->links;
else b = b->rechts;
}
}
// Rueckgabe des Zeigers auf den Knoten; NULL, falls nicht gefunden
return b;
}
public:
// Konstruktoren, Destruktoren
binSBaum(void);
binSBaum(const binSBaum<T>& baum);
~binSBaum(void);
// Zuweisungsoperator
binSBaum<T>& operator= (const binSBaum<T>& rs);
// Bearbeitungsmethoden
int finden(T& merkmal);
void einfuegen(const T& merkmal);
void loeschen(const T& merkmal);
void bereinigeListe(void);
int leererBaum(void) const;
int baumGroesse(void) const;
// baumspezifische Methoden
void aktualisieren(const T& merkmal);
baumKnoten<T> *holeWurzel(void) const;
};
253
Die Programmiersprache C++
Operationen
1. Einfügen eines Knoten
Vorstellung zur Lösung:
1. Suche nach dem Schlüsselwert
2. Falls der Schlüsselwert im binären Suchbaum gespeichert ist, erfolgt kein Einfügen.
3. Bei erfolgloser Suche wird ein neuer Baumknoten mit dem Schlüsselwert als Sohn des erreichten
Blatts eingefügt.
Die Schnittstellenfunktion void einfuegen(const T& merkmal); besitzt
folgende Definition157:
// Einfuegen "merkmal" in den Suchbaum
template <class T>
void binSBaum<T>::einfuegen(const T& merkmal)
{ // b ist der aktuelle Knoten beim Durchlaufen des Baums
baumKnoten<T> *b = wurzel, *eltern = NULL, *neuerKnoten;
// Terminiere beim leeren Teilbaum
while(b != NULL)
{ // Aktualisiere den zeiger "eltern",
// dann verzweige nach links oder rechts
eltern = b;
if (merkmal < b->daten) b = b->links;
else b = b->rechts;
}
// Erzeuge den neuen Blattknoten
neuerKnoten = holeBaumKnoten(merkmal,NULL,NULL);
// Falls "eltern" auf NULL zeigt, einfuegen eines Wurzelknoten
if (eltern == NULL) wurzel = neuerKnoten;
// Falls merkmal < eltern->daten, einfuegen als linker Nachfolger
else if (merkmal < eltern->daten) eltern->links = neuerKnoten;
else
// Falls merkmal >= eltern->daten, einfuegen als rechter Nachf.
eltern->rechts = neuerKnoten;
// Zuweisen "aktuell": "aktuell" ist die Adresse des neuen Knoten
aktuell = neuerKnoten;
groesse++;
}
2. Löschen eines Knoten
Es soll ein Knoten mit einem bestimmten Schlüsselwert entfernt werden. Dabei sind
folgende Fälle zu unterscheiden:
a) Der zu löschende Knoten ist ein Blatt, z.B.:
157
vgl. bsbaum.h
254
Die Programmiersprache C++
vorher
nachher
Das Entfernen kann leicht durchgeführt werden
b) Der zu löschende Knoten hat genau einen Sohn, z.B.:
vorher
nachher
c) Der zu löschende Knoten hat zwei Söhne, z.B.:
nachher
vorher
Der nach dem Löschen resultierende Teilbaum ist natürlich wieder ein Suchbaum,
häufig allerdings mit erheblich vergrößerter Höhe.
Aufgaben
1) Gegeben ist ein binärer Baum folgender Gestalt:
255
Die Programmiersprache C++
k
k1
k2
k3
Die Wurzel wird gelöscht. Welche Gestalt nimmt der Baum dann an:
k1
k3
k2
Es ergibt sich eine Höhendifferenz H , die durch folgende Beziehung eingegrenzt ist:
1  H  H (TL ) ( H (TL ) ist die Höhe des linken Teilbaums).
2) Gegeben ist die folgende Gestalt eines binären Baums
12
7
5
2
15
13
6
14
Welche Gestalt nimmt dieser Baum nach dem Entfernen der Schlüssel mit den unter a) bis f)
angegebenen Werten an?
a) 2 b) 6
256
Die Programmiersprache C++
12
7
15
5
13
14
c) 13
12
7
15
5
14
d) 15
12
7
14
5
e) 5
12
7
14
f) 12
7
14
Schlüsseltransfer:
Der angegebene Algorithmus zum Löschen von Knoten kann zu einer beträchtlichen
Vergrößerung der Baumhöhe führen. Das bedeutet auch eine beträchtliche
Steigerung des mittleren Suchaufwands. Man ersetzt häufig die angegebene
Verfahrensweise durch ein anderes Verfahren, das unter dem Namen
Schlüsseltransfer bekannt ist.
Der zu löschende Schlüssel (Knoten) wird ersetzt durch den kleinsten Schlüssel des
rechten oder den größten Schlüssel des linken Teilbaums. Dieser ist dann nach Fall
A) bzw. B) aus dem Baum herauszunehmen, z.B.:
257
Die Programmiersprache C++
Größter / Kleinster
Schlüsselwert
des linken / des rechten Teilbaums
Abb.: Darstellung der Verfahrensweise „Schlüsseltransfer“
Implementierung der Verfahrensweise „Schlüsseltransfer“ zum Löschen von
Baumknoten in einem binären Suchbaum:
// Falls "merkmal" im baum vorkommt, dann loesche es
template <class T>
void binSBaum<T>::loeschen(const T& merkmal)
{
// LKnoZgr: Zeiger auf Knoten L, der geloescht werden soll
// EKnoZgr: Zeiger auf die "eltern" E des Knoten L
// ErsKnoZgr: Zeiger auf den rechten Knoten R, der L ersetzt
baumKnoten<T> *LKnoZgr, *EKnoZgr, *ErsKnoZgr;
// Suche nach einem Knoten, der einen Knoten enthaelt mit dem
// Datenwert von "merkmal". Bestimme die aktuelle Adresse diese Knotens
// und die seiner "eltern"
if ((LKnoZgr = findeKnoten (merkmal, EKnoZgr)) == NULL) return;
// Falls LKnoZgr einen NULL-Zeiger hat, ist der Ersatzknoten
// auf der anderen Seite des Zweigs
if (LKnoZgr->rechts == NULL)
ErsKnoZgr = LKnoZgr->links;
else if (LKnoZgr->links == NULL) ErsKnoZgr = LKnoZgr->rechts;
// Beide Zeiger von LKnoZgr sind nicht NULL
else
{ // Finde und kette den Ersatzknoten fuer LKnoZgr aus.
// Beginne am linken Zweig des Knoten LKnoZgr,
// bestimme den Knoten, dessen Datenwert am groessten
// im linken Zweig von LKnoZgr ist. Kette diesen Knoten aus.
// EvonErsKnoZgr: Zeiger auf die "eltern" des zu ersetzenden Knoten
baumKnoten<T> *EvonErsKnoZgr = LKnoZgr;
// erstes moegliches Ersatzstueck: linker Nachfolger von L
ErsKnoZgr = LKnoZgr->links;
// steige den rechten Teilbaum des linken Nachfolgers von LKnoZgr hinab,
// sichere den Satz des aktuellen Knoten und den seiner "Eltern"
// Beim Halt, wurde der zu ersetzende Knoten gefunden
while(ErsKnoZgr->rechts != NULL)
{
EvonErsKnoZgr = ErsKnoZgr;
ErsKnoZgr = ErsKnoZgr->rechts;
}
if (EvonErsKnoZgr == LKnoZgr)
// Der linke Nachfolger des zu Beschreibenden Knoten ist das
// Ersatzstueck
// Zuweisung des rechten Teilbaums
ErsKnoZgr->rechts = LKnoZgr->rechts;
else
{ // es wurde sich um mindestens einen Knoten nach unten bewegt
// der zu ersetzende Knoten wird durch Zuweisung seines
// linken Nachfolgers zu "Eltern" geloescht
EvonErsKnoZgr->rechts = ErsKnoZgr->links;
258
Die Programmiersprache C++
// plaziere den Ersatzknoten an die Stelle von LKnoZgr
ErsKnoZgr->links = LKnoZgr->links;
ErsKnoZgr->rechts = LKnoZgr->rechts;
}
}
// Vervollstaendige die Verkettung mit den "Eltern"-Knoten
// Loesche den Wurzelknoten, bestimme eine neue Wurzel
if (EKnoZgr == NULL) wurzel = ErsKnoZgr;
// Zuweisen Ers zum korrekten Zweig von E
else if (LKnoZgr->daten < EKnoZgr->daten)
EKnoZgr->links = ErsKnoZgr;
Else EKnoZgr->rechts = ErsKnoZgr;
// Loesche den Knoten aus dem Speicher und erniedrige "groesse"
freigabeKnoten(LKnoZgr);
groesse--;
}
Test: Die nun bekannte Prozedur zum Löschen vom Baumknoten nach der
Verfahrensweise des "Schlüsseltransfer" ist folgenden Prüfungen zu unterwerfen:
1) Der zu löschende Baumknoten besteht nur aus einem Wurzelknoten, z.B.:
Schlüssel
LINKS
12
RECHTS
Ergebnis: Der Wurzelknoten wird gelöscht.
2) Vorgegeben ist
Schlüssel
LINKS
12
RECHTS
7
5
8
Der Wurzelknoten wird gelöscht.
Ergebnis:
259
Die Programmiersprache C++
7
5
8
3) Vorgegeben ist
Schlüssel
12
LINKS
RECHTS
7
5
15
8
13
14
Abb.:
Der Wurzelknoten wird gelöscht.
Ergebnis:
260
Die Programmiersprache C++
Schlüssel
LINKS
13
RECHTS
7
5
15
8
14
Abb.:
Ordnungen und Durchlaufprinzipien
Das Prinzip, wie ein geordneter Baum durchlaufen wird, legt eine Ordnung auf der
Menge der Knoten fest. Es gibt 3 Möglichkeiten (Prinzipien), die Knoten eines
binären Baums zu durchlaufen:
1. Inorder-Durchlauf
LWR-Ordnung
(1) Durchlaufen (Aufsuchen) des linken Teilbaums in INORDER
(2) Aufsuchen der BAUMWURZEL
(3) Durchlaufen (Aufsuchen) des rechten Teilbaums in INORDER
RWL-Ordnung
(1) Durchlaufen (Aufsuchen) des rechten Teilbaums in INORDER
(2) Aufsuchen der BAUMWURZEL
(3) Durchlaufen (Aufsuchen) des Teilbaums in INORDER
Der LWR-Ordnung und die RWL-Ordnung sind zueinander invers. Die LWR Ordnung heißt auch symmetrische Ordnung.
2. Präorder-Durchlauf
WLR-Ordnung
(1) Aufsuchen der BAUMWURZEL
(2) Durchlaufen (Aufsuchen) des linken Teilbaums in PREORDER
(3) Durchlaufen (Aufsuchen) des rechten Teilbaums in PREORDER
261
Die Programmiersprache C++
WRL-Ordnung
(1) Aufsuchen der BAUMWURZEL
(2) Durchlaufen (Aufsuchen) des rechten Teilbaums in PREORDER
(3) Durchlaufen (Aufsuchen) des linken Teilbaums in PREORDER
Es wird hier grundsätzlich die Wurzel vor den (beiden) Teilbäumen durchlaufen.
3. Postorder-Durchlsauf
LRW-Ordnung
(1) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER
(2) Durchlaufen (Aufsuchen) des rechten Teilbaums in POSTORDER
(3) Aufsuchen der BAUMWURZEL
Zuerst werden die beiden Teilbäume und dann die Wurzel durchlaufen.
RLW-Ordnung
(1) Durchlauden (Aufsuchen) des rechten Teilbaums in POSTORDER
(2) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER
(3) Aufsuchen der BAUMWURZEL
Zuerst werden die beiden Teilbäume und dann die Wurzel durchlaufen.
Funktionsschablonen für das Durchlaufen binärer Bäume
// Funktionsschablonen fuer Baumdurchlaeufe
template <class T> void inorder(baumKnoten<T>* b,
void aufsuchen(T& merkmal))
{
if (b != NULL)
{
inorder(b->holeLinks(),aufsuchen);
aufsuchen(b->daten);
inorder(b->holeRechts(),aufsuchen);
}
}
template <class T> void postorder(baumKnoten<T>* b,
void aufsuchen(T& merkmal))
{
if (b != NULL)
{
postorder(b->holeLinks(),aufsuchen);
// linker Abstieg
postorder(b->holeRechts(),aufsuchen);
// rechter Abstieg
aufsuchen(b->daten);
}
}
Aufgaben: Gegeben sind eine Reihe binärer Bäume. Welche Folgen entstehen beim
Durchlaufen der Knoten nach den Prinzipien "Inorder (LWR)", "Praeorder WLR" und
"Postorder (LRW)".
262
Die Programmiersprache C++
1)
A
B
C
E
D
F
I
G
J
H
K
L
"Praeorder": A B C E I F J D G H K L
"Inorder":
EICFJBGDKHLA
"Postorder": I E J F C G K L H D B A
2)
+
*
A
+
B
*
C
E
D
"Praeorder": + * A B + * C D E
"Inorder":
A*B+C*D+E
"Postorder": A B * C D * E + +
Diese Aufgabe zeigt einen Strukturbaum (Darstellung der hierarchischen Struktur
eines arithmetischen Ausdrucks). Diese Baumdarstellung ist besonders günstig für
die Übersetzung eines Ausdrucks in Maschinensprache. Aus der vorliegenden
Struktur lassen sich leicht die unterschiedlichen Schreibweisen eines arithmetischen
Ausdrucks herleiten. So liefert das Durchwandern des Baums in „Postorder" die
Postfixnotation, in „Praeorder“ die Praefixnotation.
263
Die Programmiersprache C++
3)
+
A
*
B
C
"Praeorder": + A * B C
"Inorder":
A+B*C
"Postorder": A B C * +
4)
+
*
A
C
B
"Praeorder": * + A B C
"Inorder":
A+B*C
"Postorder": A B + C *
Balancierte Bäume
Strukturbäume
264
Die Programmiersprache C++
5. C- und C++-Bibliotheken
5.1 Die C++-Standardbibliothek und die STL
Die STL (Standard Template Library158) unfaßt nicht die ganze C++Standardbibliothek und auch nicht alle ihre Templates, sie stellt aber den wichtigsten
und interessantesten Teil dar.
Hilfsfunktionen und -klassen
Container
Iteratoren
Algorithmen
Ein-/Ausgabe
Nationale Besonderheiten
Numerisches
String
Laufzeittyperkennung
Fehlerbehandlung
Speicher
C-Header
Header
<utility>
<functional>
<bitset>
<deque>
<list>
<map>
<queue>
<set>
<stack>
<vector>
<iterator>
<algorithm>
<fstream>
<iomanip>
<ios>
<istream>
<ostream>
<sstream>
<streambuf>
<complex>
<limits>
<numerics>
<valarray>
<string>
<typeinfo>
<exception>
<stdexcept>
<memory>
<new>
<cassert>
<cctype>
<cerrno>
<cfloat>
<ciso646>
<clocale>
<cmath>
<csetjmp>
<csignal>
<cstdarg>
<cstdef>
<cstdio>
<cstdlib>
<cstring>
158
Die STL wurde bei Hewlett-Packard von Alexander Stepanow, Meng Lee und ihren Kollegen entwickelt. Sie
wurde vom Ansi-/ISO-Komitee als Teil des C++-Standards (ISO/IEC 14882) akzeptiert. Ihr Schwerpunkt liegt
auf Datenstrukturen für Container und Algorithmen, die damit arbeiten.
265
Die Programmiersprache C++
<ctime>
<cwchar>
<cwctype>
Abb.: Aufbau der Standardbibliothek
Die STL besteht aus drei Komponenten: Container (Auflistungen), Algorithmen
und Iteratoren.
Ein Container ist eine Datenstruktur zur Speicherung und Organisation von Daten.
STL-Container sind als Template-Klassen implementiert.
Ein Algorithmus aus der STL definiert eine Verhaltensweise eines Container und
verwendet gewisse Funktionen auf einen Container an, um dessen Inhalt irgendwie
zu be- oder zu verarbeiten, z.B. sortieren, kopieren. In der STL werden Algorithmen
durch Template-Funktionen repräsentiert. Diese Funktionen sind keine MemberFunktionen der Container-Klassen sondern eigenständige Funktionen. Sie können
nicht nur mit STL-Containern, sondern auch mit gewöhnlichen C-Arrays oder
anwendungsspezifischen Containern arbeiten.
Ein Iterator kann als ein verallgemeinerter Zeiger betrachtet werden, der auf
Elemente in einem Container zeigt. Ein Iterator kann wie ein Zeiger inkrementiert
werden, um auf das nächste Element in dem Container zu zeigen.
5.1.1 Die C-Standard-Library
C-Header wurden von der Programmiersprache C übernommen. Ihr Inhalt entspricht
der C-Standard-Library (ISO 90). Die Dateinamen ergeben sich aus dem HeaderNamen, so heißt bspw. die Datei zum Header <cmath> dementsprechend math.h.
<cassert>
Zusicherungen werden mit <cassert> eingebunden.
<cctype>
In den meisten Sprachen bestehen Zeichen aus einem einzigen Byte. Die Makros
und Funktionen zur Manipulation von Zeichen sind komplett oder als Prototyp in
cctype enthalten. Sie haben int-Argumente, verwenden aber nur das untere Byte der
int-Werte. Wegen der automatischen Typumwandlung können normalerweise auch
Zeichenargumente an diese Makros und Funktionen übergeben werden.
Der Header enthält die in den folgenden Tabellen aufgeführten Funktionen zum
Klassifizieren und Umwandeln von Zeichen:
Schnittstelle
tolower(z)
toupper(z)
toascii(z)
Bedeutung
Gibt z als Kleinbuchstaben zurück
Gibt z als Großbuchstaben zurueck
Gibt z als ASCII-Zeichen zurück
Abb.: Umwandlungsfunktionen aus <cctype>
Schnittstelle
isalnum(z)
isalpha(z)
isascii(z)
Wahr, wenn z ==
Buchstabe oder Ziffer
Buchstabe
ganzzahliger Wert
Bereich
A..Z, a..z, 0..9
A..Z, a..z
ASCII-Werte, 0 .. 127
266
Die Programmiersprache C++
iscntrl(z)
isdigit(z)
isgraph(z)
islower(z)
isprint(z)
ispunct(z)
Steuerzeichen
Ziffer
Druckbares Zeichen (ohne ‘‘)
Kleinbuchstabe
Druckbares Zeichen (mit ‘‘)
Druckbar, aber weder ‘‘ noch alphanumerisch
isspace(z)
isupper(z)
isxdigit(z)
Zwischenraumzeichen
Großbuchstabe
Hexadezimale Ziffer
0x00..0x1f,0x7f
0..9
0x21..0x7e
a..z
0x20..0x7e
0x21..0x2f,0x3a..0x40,
0x5b..0x7e
0x09..0x0d
A..Z
0..9, A..F, a..f
Klassifizierungsfunktionen aus <cctype>
<cerrno>
Im Header <cerrno> wird eine globale Variable errno deklariert, deren Wert von
vielen Systemfunktionen im Fehlerfall gesetzt wird.
<cmath>
Die mathematischen Funktionen der folgenden Tabelle sind im Header <cmath> für
Grunddatentypen zu finden.
Schnittstelle
F159 abs(F x)
F acos(F x)
F asin(F x)
F atan(F x)
F atan2(F x, F y)
F ceil(F x)
F cos(F x)
F cosh(F x)
F exp(F x)
F fabs(F x)
F floor(F x)
F fmod(F x, F y)
F frexp(F x, int* pn)
F ldexp(F x, int* pn)
F log(F x)
F log10(F x)
F modf(F x, F* i)
F pow(F x, Fy)
F pow(F x, int y
F sin(F x)
F sinh(F x)
F sqrt(F x)
F tan(F x)
F tanh(F x)
Mathematische Entsprechung
arctan(x/y)
ln x
sinh x
tan x
tanh x
Abb.: Mathematische Funktionen
<csignal>
<cstddef>
Der Header <cstddef> enthält Standarddefinitionen des jeweiligen Systems.
159
Die Abkürzung F bedeutet: Einer der Typen float, double oder long double
267
Die Programmiersprache C++
Name
size_t
ptrdiff_t
wchar_t
offsetof
NULL
Bedeutung
Vorzeichenloser Ganzzahlentyp für das Ergebnis von sizeof
Ganzzahliger Typ mit Vorzeichen zur Substraktion von Zeigern
Typ für „wide characters“. Wide characters sind für Zeichensätze gedacht, bei denen ein
Byte nicht ausreicht.
Abstand eines Strukturelements vom Strukturanfang in Bytes
Null-Zeiger (dasselbe wie 0 oder 0L)
Abb.: Standarddefinitionen aus <cstddef>
<cstdarg>
Funktionen mit Argumentlisten variabler Länge enthalten eine Ellipse in der
Parameterliste, z.B. (int ...). Die Funktionen benötigen die Datentypen und
Makros des Header <cstdarg>.
<cstdlib>
Die erste wichtige Gruppe von Funktionen in cstdlib sind Funktionen zur
Umwandlung von Daten. Ihre Hauptaufgabe besteht darin, Daten von einem
Datentyp in einen anderen umzuwandeln.
Funtionsprototyp
double atof(const char *s)
int atoi(const char *s)
long atol(const char *s)
char *ecvt(double wert,int n, int *dec, int *sign)
char *fcvt(double wert,int n, int *dec, int *sign)
char *gcvt(double value, int n, char *buf)
char *iota(int value, char *s, int radix)
char *ltoa(long value, char *s, int radix)
double strtod(const char *s, char **endptr)
long strtol(const char *s, char **endptr, int radix)
unsigned long strtoul(const char *s, char **endptr, int radix)
char *ultoa(unsigned long value, char *s, int radix)
Beschreibung
wandelt einen einen String in einen float-Wert
wandelt einen einen String in einen int-Wert
wandelt einen einen String in einen long-Wert
wandelt einen String in einen double-Wert
wandelt einen String in einen long-Wert
wandelt einen String in einen unsigned long-Wert
Abb.: Funktionsprototypen von Umwandlungsfunktionen
Die folgenden mathematischen Funktionen gehören zum Header <cstdlib>
Schnittstelle
int abs(int x)
long abs(long x)
long labs(long x)
div_t160 div(int z, int n)
ldiv_t div(long z, long n)
ldiv_t ldiv(long z, long n)
int rand()
Mathematische Entsprechung
Betrag
Betrag
Betrag
Pseudozufallszahl zwischen 0 und RAND_MAX.
RAND_MAX ist die größtmögliche Pseudozufallszahl
Initialisiert den Zufallszahlengenerator
void srand(unsigned seed)
Abb.: Mathematische Funktionen aus <cstdlib>
160
div_t ist eine vordefinierte Struktur, die das Divisionsergebnis und den Rest enthält:
struct div_t
{
int quot; // Qotient
int rem; // Rest
}
268
Die Programmiersprache C++
Weitere Funktionen für diverse Oprationen
Schnittstelle
void abort(void)
void atexit(void (*f)())
Bedeutung
gibt den Exit-Code 3 zurück
Trägt die Funktion f in eine Liste von Funktionen
ein, die vor dem normalem Programmende
aufgerufen werden.
void exit(int status)
Normales Programmende, der Status wird an das
Betriebssystem gemeldet (Exit-Code 0)
int system(const char* B)
Befehl B an den Kommandointerpreter des
Betriebssystems geben
char* getenv(const char* E)
Wert der Environmemt-Variablen E
long atoi(const char* s)
Interpretiert den C-String s als int-Zahl
long int atol(const char* s)
Interpretiert den C-String s als long-Zahl
double atof(const char* s)
Interpretiert den C-String s als double-Zahl
void* bsearch(const void* key, const Binäre Suche, key zeigt auf den Schlüssel, der
void*
base,
size_t
size,
int im Feld base mit n Elementen gesucht wird. Die
(*cmp)(const void*, const void*))
Größe der Feldelemente ist size, die
Vergleichsfunktion ist cmp.
void qsort(void*, size_t, size_t, int Quicksort
(*) const void*, const void*))
void lfind(const void *key, const Durchsuchen
eines
Array
sequentieller
void *base, size_t *, size_t width, Datensätze linear nach einem Schlüsselbegriff
int (*fcmp)(const void *, const void
*))
void lsearch(const void *key, const Durchsuchen einer sortierten oder unsortierten
void *base, size_t *, size_t width, Tabelle linear nach einem Schlüsselbegriff
int (*fcmp)(const void *, const void
*))
Abb.: Ausgewählte Funktionen aus <cstdlib>
<cstring>
String-Funktionen, deren Prototypen in cstring stehen verwenden üblicherweise
Zeigerargumente und geben Zeiger oder ganzzahlige Werte zurück
Funktion
int strcmp(const char *s1, const char *s2)
size_t strcspn(const char *s1, const char *s2)
char *strcpy(char *s1, char *s2)
char *strerror(int errnum)
size_t strlen(const char *s)
char *strncat(char *s1, const char *s2, size_t n)
char strncmp(const char *s1, char* s2, size_t n)
char strnicmp(const char *s1, char* s2, size_t n)
char *strncpy(char *s1, const char *s2, size_t n)
char *strnset(char *s1, int ch, size_t n)
char strpbrk(const char *s1, const char *s2)
char *strrchr(const char *s, int ch)
char *strrev(char *s)
char *strset(char *s, int ch)
size_t strspn(const char *s1, const char *s2)
char *strstr(const char *s1, const char *s2)
char *strtok(char *s1, char *s2)
char *strupr(char *s)
Beschreibung
vergleicht zwei Strings
findet einen Substring in einem String
kopiert einen String
ANSI-spez. Fehlernummer
wandelt einen String in Kleinbuchstaben um
hängt n in char s2 an s1 an
vergleicht die ersten n Zeichen zweier Strings
vergleicht die ersten n Zeichen zweier Strings,
Groß- /Kleinschreibung spielt keine Rolle
kopiert n Zeichen von s2 nach s1
setzt die ersten n Zeichen eines String auf "ch"
findet Zeichen aus s2 in s1
findet das letzte Zeichen von "ch" im String
kehrt die Zeichen eines String um
setzt alle Zeichen eines Strings auf "ch"
durchsucht s1 nach Zeichen in s2
durchsucht s1 nach s2
durchsucht s1 nach Token, s1 enthält das / die
Token, s2 enthält die Begrenzer
wandelt einen String in Großbuchstaben um
Abb.: Funktionen zur Manipulation von Strings
269
Die Programmiersprache C++
<ctime>
Der Header <ctime> enthält verschiedene Funktionen zur Bearbeitung und
Auswertung der Systemzeitinformation.
Datentypen: Neben bekannten Typen NULL und size_t sind definiert:
clock_t
time_t
struct tm
int
int
int
int
int
int
int
int
int
// Datentyp für CPU-Zeitangaben
// Datentyp für Datums- und Zeitangaben
// Struktur mit mindestens folgenden Elementen
tm_sec
// Sekunden 0 .. 59
tm_min
// Minuten 0 .. 59
tm_hour
// Stunden 0 .. 23
tm_mday
// Monatstag 1 .. 31
tm_mon
// Monat 0.. 11
tm_year
// Jahr seit 1900
tm_wday
// Wochentag seit Sonntag 0 .. 6
tm_yday
// Tag seit 1. Januar 0 .. 365
tm_isdst // is daylight saving time, Werte:
// Sommerzeit (> 0), Winterzeit (0)
// undefiniert (-1)
Funktionen:
char * asctime(const tm*)
char * ctime(const time_t*)
Die Funktionen wandeln die im tm-Format oder
time_t Format vorliegende Zeit in einen
formatierten C-String um.
clock_t clock()
Gibt die seit Programmstart verstrichene CPU-Zeit
in „Ticks“ zurück. Die „Ticks“ können in Sekunden
umgerechnet
werden,
wenn
durch
die
vordefinierte
Konstante
CLOCKS_PER_SEC
dividiert wird
double difftime(time_t t1, time_t t2) Ermittelt die Differenz der Zeiten in Sekunden
size_t strftime(char* puffer, size_t Wandelt die im tm-Format vorliegende Zeit in
max, const char* format, const tm* z) einen formatierten C-String um, wobei das
Ergebnis im Puffer puffer abgelegt wird und
format einen C-String mit Formatangaben
darstellt. Die Anzahl der in den Puffer
geschriebenen Zeichen wird zurückgegeben, falls
sie < max ist, ansonsten ist das Ergebnis der
Funktion 0. Die in einem Unix-Sytem möglichen
Formate können mit man strftime erfragt
werden. Formatierungsfunktionen umfassen:
%a Abgekürzter Name des Wochentags
%A Kompletter Name des Wochentags
%b Abgekürzter Name des Monats
%B Kompletter Name des Monats
%c Datums- und Zeitinformationen
%d Tag des Monats (01 bis 31)
%H Stunde (00 bis 23))
%I Stunde (00 bis 12)
%j Tag des Jahres (001 bis 366)
%m Monat (01 bis 12)
%M Minuten (00 bis 59)
%S Sekunden (00 bis 59)
%w Wochentag (0 bis 6)
%x Datum
%X Zeit
%Y Jahr mit Jahrhundert
%Z Name der Zeitzone
%% Zeichen %
tm* gmtime(const time_t* z)
Beide Funktionen wandeln die in *z vorliegende
tm* localtime(const time_t* z)
270
Die Programmiersprache C++
time_t time(time_t* z)
Zeit in die Struktur tm um. Dabei gibt
localtime()
die
lokale
Ortszeit
unter
Berücksichtigung von Sommer- und Winterzeit
zurück, während gmtime() die UTC (Universal
Time Coordinated, entspricht GMT (Greenwich
mean Time)) zurückgibt.
Gibt die momentane Kalenderzeit zurück (die Zeit
in Sekunden seit dem 1. Jan. 2000, 00:00:00
GMT) bzw. –1 bei Fehler. Falls z ungleich NULL
ist, wird der Rückgabewert an der Stelle z
hinterlegt.
Abb.: Parameter der Datums- und Zeitfunktionen
Der Umgebungsstring TZ hat folgende Syntax:
TZ = zzz[+/-]d[d]{lll}
zzz String aus drei Zeichen, der die lokale Zeitzone, bspw. EST für Eastern Standard Time, angibt.
[+/-]d[d] enthält eine Anpassung für den Unterschied der lokalen Zeitzone und der GMT
(Grennwich Mean Time). Positive Zahlen bedeuten eine Anpassung in westliche, negative Zahlen eine
Anpassung in östliche Richtung.
{lll} Anpassung der lokalen Zeitzone an die Sommer-/Winterzeit
271
Die Programmiersprache C++
5.1.2 Hilfsfunktionen und -klassen
Der Header <utility> deklariert verschiedene Templates von Hilfsfunktionen und
–klassen, die in der ganzen Bibliothek benutzt werden.
5.1.2.1 Paare
Das Template pair erlaubt die Kombination heterogener Wertepaare:
template <class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair();
pair(const T1& x, const T2& y);
template<class U, class V> pair(const pair<U, V> &p);
};
Der Standardkonstruktor initialisiert beide Bestandteile mit ihrem jeweiligen
Standardkonstruktor. Gleichheit und Vergleich sind ebenfalls definiert:
// gibt first == y.first und x.second == y.second zurück
template <class T1, class T2>
bool operator ==(const pair<T1, T2>& x, const pair<T1, T2>& y);
template <class T1, class T2>
bool operator <(const pair<T1, T2>& x, const pair<T1, T2>& y);
Rückgabewert true, falls x.first < y.first bzw. false, falls y.first < x.first. Falls
beide Bedingungen nicht zutreffen, wird x.second < y.second zurückgegeben.
Die Hilfsfunktion
template <class T1, class T2> pair<T1, T2> make_pair(const T1& x, const T2&
y);
erzeugt aus den Parametern ein Paar. Die beteiligten Typen werden aus den
Parametern abgeleitet, z.B.:
return make_pair(1,2.7); gibt ein pair<int, double>-Objekt zurück.
5.1.2.2 Funktionsobjekte
In einem Ausdruck wird der Aufruf einer Funktion durch das von der Funktion
zurückgegebene Ergebnis ersetzt. Die Ausgabe der Funktion kann von einem Objekt
übernommen werden. Dazu wird der Funktionsoperator () mit der Operatorfunktion
operator()() überladen. Ein Objekt kann dann wie eine Funktion aufgerufen
werden. Ein algorithmisches Objekt dieser Art wird Funktionsobjekt oder Funktor
genannt.
272
Die Programmiersprache C++
Funktoren sind Objekte, die sich wie Funktionen verhalten, aber alle Eigenschaften
von Objekten haben. Sie können erzeugt, als Parameter übergeben oder in ihrem
Zustand verändert werden.
In <functional> sind Klassen definiert, die zum Erzeugen verschiedener
Funktionsobjekte dienen.
Klassen dieses Header erben von einer der Klassen:
template <class Arg, class Result> struct unary_function
{
typedef Arg argument_type;
typedef Result result_type;
}
template <class Arg1, class Arg2, class Result> struct binary_function
{
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
}
in Abhängigkeit davon, ob es sich um unäre oder binäre Operationen handelt, z.B.:
template <class T> struct plus : binary_function<T,T,T>
{
T opertor()(const T& x, const T& y) const;
}
Arithmetische, vergleichende und logische Operationen
In der STL werden für alle möglichen Grundoperationen Funktionsobjekte
vordefiniert.
// arithmetische Operationen:
template
template
template
template
template
template
<class
<class
<class
<class
<class
<class
T>
T>
T>
T>
T>
T>
struct
struct
struct
struct
struct
struct
plus;
minus;
multiplies;
divides;
modulus;
negate;
//
//
//
//
//
//
x +
x x *
x /
x %
-x
y
y
y
y
y
T>
T>
T>
T>
T>
T>
T>
struct
struct
struct
struct
struct
struct
struct
equal_to;
// x == y
not_equal_to; // x != y
greater;
// x > y
less;
// x < y
greater_equal; // x >= y
less_equal;
// x <= y
equal_to; // x == y
// Vergleiche
template
template
template
template
template
template
template
<class
<class
<class
<class
<class
<class
<class
// logische Operationen
template <class T> struct logical_and;
template <class T> struct logical_or;
template <class T> struct logical_not;
// x && y
// x || y
// !x
Funktionsobjekte zum Negieren logischer Prädikate
„not1()“ und „not2()“ sind Funktionen, die ein Funktionsobjekt zurückgeben,
dessen Aufruf ein Prädikat negiert.
273
Die Programmiersprache C++
Binden von Argumentwerten
Diese Funktionen wandeln binäre in unäre Funktionsobjekte um, indem eines der
beiden Argumente an einen Wert gebunden wird. Sie akzeptieren ein
Funktionsobjekt mit zwei Argumenten und einem Wert x. Sie liefern ein unäres
Funktionsobjekt zurück, dessen erstes („Funktionsadapter bind1st()“) bzw.
zweites Argument („Funktionsadapter161 bind2nd()“) an den Wert x gebunden ist.
Bsp.: Alle Elemente einer Menge mit 3 multiplizieren
// Elemente mit 3 mutiplizieren
transform(v.begin(), v.end(),
ostream_iterator<int>(cout," "),
bind2nd(multiplies<int>(),3));
// Quellbereich
// Zielbereich
// Operation
Der Ausdruck bind2nd(multiplies<int>(),3)) dient dazu die zweistellige
Multiplikation, die durch multiplies<int>() definiert wird, mit dem Wert 7 zu
einer einstelligen Operation, wie sie von transform() her erwartet wird, zu
verknüpfen. Mit dem gleichen Prinzip wird dem Iterator pos das erste Element mit
einem Wert, der kleiner ist 5, zugewiesen:
// Erstes Element mit Wert kleiner als 5 finden
vector<int>::iterator pos;
pos = find_if(v.begin(), v.end(),bind2nd(less<int>(),5));
Bsp.: Anwendung eines selbstdefinierten Funktionsobjekts
Funktionsadaptern bind1st() und bind2nd().
hoch
mit
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/* Selbstdefiniertes Funktionsobjekt hoch */
#include "fohoch.h"
int main()
{
vector<int> v;
// Elemente 1 bis 6 in v einfuegen
for (int i = 1; i <= 6; i++)
v.push_back(i);
// Elemente mit 3 mutiplizieren
transform(v.begin(), v.end(),
ostream_iterator<int>(cout," "),
bind2nd(multiplies<int>(),3));
cout << endl;
// 3 hoch alle Elemente ausgeben
transform(v.begin(), v.end(),
ostream_iterator<int>(cout," "),
bind1st(hoch<int>(),3));
cout << endl;
// Alle Elemente hoch 3 ausgeben
transform(v.begin(), v.end(),
ostream_iterator<int>(cout," "),
// Quellbereich
// Zielbereich
// Operation
// Quellbereich
// Zielbereich
// Operation
// Quellbereich
// Zielbereich
161
Funktions-Adapter ermöglichen die vordefinierten Funtionsobjekte zu kombinieren oder mit bestimmten
Werten zu versehen. Auch sie werden in der Header-Datei functional definiert
274
den
Die Programmiersprache C++
bind2nd(hoch<int>(),3));
cout << endl;
// Operation
}
Funktionsobjekte können auch selbst definiert werden. Damit auf sie auch
Funktionsadapter angewendet werden können, müssen sie von den vordefinierten
Strukturen unary_function bzw. binary_function abgeleitet werden.
#include <functional>
template <class T>
struct hoch : public binary_function<T, int, T>
{
T operator() (const T& basis, int exp) const
{
T res = 1;
while (exp > 0)
{
res = res * basis;
--exp;
}
return res;
}
};
Zeiger auf Funktionen in Objekte umwandeln
Die Funktion ptr_fun() wandelt einen Zeiger auf eine Funktion in ein
Funktionsobjekt um. Die Funktion kann ein oder zwei Parameter haben.
Bsp.:
275
Die Programmiersprache C++
5.2 Container
Die Container-Klassen dienen dazu, eine Menge von Elementen in einer bestimmten
Art und Weise zu verwalten. Da an Mengen verschiedene Anforderungen gestellt
werden, gibt es auch verschiedene Container-Klassen, die diese Anforderungen
erfüllen.
Sequentielle und assoziative Container
Die Container werden in sequentielle und assoziative Container unterteilt:
-
-
Sequentielle Container sind geordnete Mengen, in denen jedes Element eine bestimmte Position
besitzt, die durch den Zeitpunkt und den Ort des des Einfügens bestimmt wird.
Vordefinierte sequentielle Container sind Vektoren, Deques und Listen.
Assoziative Container sind sortierte Mengen, bei denen die Position der Elemente durch ein
Sortierkriterium bestimmt wird.
Werden bspw. 6 Elemente nacheinander in eine Menge eingefügt, besitzen sie eine Reihenfolge,
die durch ihren Wert definiert wird.
Vordefinierte assoziative Container sind Sets, Multisets sowie Maps, Multimaps.
Neben den fundamentalen Container-Klassen existieren noch spezielle ContainerAdapter, die die fundamentalen Container auf spezielle Anforderungen abbilden:
Stack, Queue, Priority Queue. Die Container-Adapter sind mit der STL in den
Standard aufgenommen, für die Arbeit mit Iteratoren und Algorithmen stehen sie
aber nicht zur Verfügung. Sie bilden normale Container-Klassen.
Soweit möglich besitzen alle Container die gleiche Schnittstelle. Die Schnittstelle
wird (leider) nicht in einer abstrakten Klasse definiert sondern durch Tabellen, in der
die gemeinsamen Eigenschaften aller Container festgelegt werden.
Container-Typen
Datentyp
container::value_type
container::reference
container::const_reference
container::iterator
container::const_iterator
container::difference-type
container::size_type
Bedeutung
Liefert den Datentyp der Elemente im Container
Vorhanden bei: Vektoren, Deques. Listen, Sets, Multisets,
Maps, Multimaps
Liefert den Datentyp der Elemente im Container als Referenz
Vorhanden bei: vektoren, Deques, Listen, Sets, Maps,
Multimaps
Referenz auf konstantes Container-Element
Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets,
Maps, Multimaps
Liefert den Datentyp für Iteratoren des Containers
Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets,
Maps, Multimaps
Iteratortyp für Container mit konstanten Elementen
Vorhanden bei Vektoren, Deques, Listen, Sets, Multisets,
Maps, Multimaps
Liefert
den
Datentyp
für
Abstandsabgeben
(vorzeichenbehafteter
ganzzahliger
Datentyp)
des
Containers
Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets,
Maps, Multimaps
Liefert den Datentyp für Größenbagaben des Containers
276
Die Programmiersprache C++
container::reserve_iterator
container::
const_reserve_iterator
container::key_type
container::key_ompare
container::value_compare
Vorhanden bei: Vektoren, Deques, Listen, sets, Multisets,
Maps, Multimaps
Liefert den Datentyp für Reserve-Iteratoren des Containers
Vorhanden bei Vektoren, Deques, Listen, Sets, Multisets,
Maps, Multimaps
Liefert den Datentyp für Reserve-Iteratoren eines konstanten
Containers
Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets,
Maps, Multimaps
Liefert den Datentyp für das Sortierkriterium bei assoziativen
Containern (bei Sets und Multisets ist gleich value_type)
Vorhanden Sets, Multisets, Maps, Multimaps
Liefert den Datentyp für für Vergleichsfunktionen der
Schlüssel bei assoziativen Containers
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert den Datentyp für für Vergleichsfunktionen der
Elemente bei assoziativen Containers,
bei Sets und Multisets ist dies gleich key_compare,
bei Maps und Multimaps ist es eine Hilfsklasse, die dafür
sorgt, das von den Elementen nur der Schlüsselwert
verglichen wird.
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Abb.: Container-Datentypen, die von der C++-Standardbibliothek bereitgestellt werden
Jeder Container stellt einen öffentlichen Satz von Methoden zur Verfügung.
Methoden zum Erzeugen, Kopieren und Zerstören
Methode
container::container()
Bedeutung
Default-Konstruktor,
erzeugt
einen
leeren
Container.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
explicit
Copy-Konstruktor
container::container(const container& Erzeugt einen neuen Container als Kopie des
c)
existierenden Containers c, für jedes Element in c
wird der Copy-Konstruktoe aufgerufen.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
explicit
Erzeugt einen Container mit anz Elementen, die
container::container(size_type anz)
elemente werden jeweils mit deren DefaultKonstruktor erzeugt.
Vorhanden bei: Vektoren, Deques Listen
container::container(size_type anz,
Erzeugt einen Container mit anz Elementen, die
const T& wert)
Elemente sind jeweils Kopien von Wert. T ist der
Datentyp der Elemente.
Vorhanden bei: Vektoren, deques, Listen.
container::container(InputIterator
Erzeugt einen Container mit einer Kopie aller
anf, InputIterator end)
Elemente im Bereich [anf,end).
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
container::~container()
Destruktor, zerstört alle elemente und gibt den
Speicherplatz frei.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
Funktionen zur Größe
277
Die Programmiersprache C++
Methode
size_type container::size() const
Bedeutung
Liefert die aktuelle Anzahl von Elementen im
Container
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
bool container::empty() const
Liefert, ob der Container leer ist
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
size_type container::max_size() const Liefert die maximale Anzahl von Elementen, die
der Container enthalten kann
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
Kapazitäts-Funktionen
Methode
Bedeutung
size_type container::capacity() const Liefert die Anzahl von Elementen, die ohne
Reallokierung aufgenommen werden können
Vorhanden bei: Vektoren, Strings
void container::reserve(size_type
Reserviert für einen Vektor Speicherplatz für
anz)
mindestens anz Elemente, sofern nicht schon
genug Speicherplatz zur Verfügung steht
Vorhanden bei: Vektoren, Strings
Vergleichsoperatoren
Für alle Container-Klassen der Standard-Template-Library werden die
Vergleichsoperatoren == und < definiert. Andere Vergleiche werden auf diese
Operatoren umgesetzt.
Methode
bool operator == (const
c1, const container& c2)
Bedeutung
container& Prüft die Container c1 und c2 auf Gleichheit, liefert
true, wenn die beiden Container die gleiche
Anzahl von Elementen Besitzen und jedes
Element in c1 == dem entsprechenden element in
c2 ist.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
bool operator < (const container& c1, Prüft, ob der Container c1 „kleiner als“ c2 ist.
const container& c2)
Liefert true, wenn der erste Container „lexikalisch
kleiner“ als der zweite ist.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
Zuweisungen
Methode
container& container::operator =
(const container& c)
void container::assign(Size anz)
Bedeutung
Weist dem Container die Elemente des
Containers c zu
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
Weist dem Container anz Elemente zu, die mit
deren Default-Konstruktor erzeugt werden
Vorhanden bei: Vektoren, Deques, Listen
278
Die Programmiersprache C++
void
container::assign(Size
anz,
const T& wert)
void container::assign(InputIterator
anf, InputIterator end)
Weist dem Container anz Kopien von wert zu
Vorhanden bei: Vektoren, Deques, Listen, Strings
Weist dem Container alle Elemente im Bereich
[anf,end) zu
Vorhanden bei: Vektoren, Deques, Listen, Strings
void container::swap(container& c)
Vertauscht die Elemente bei dem Container c
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
void container::swap(container& c1, Vertauscht die Elemente des Containers c1 mit
container& c2)
den Elementen vom Container c2
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
Direkter Element-Zugriff
Methode
reference container::at(size_type
idx)
const_reference
container::at(size_type idx) const
reference
container::operator[](size_type idx)
const_reference container::operator
[](size_type idx) const
T& map::operator[](const key_type
key)
const T& map::operator[](const
key_type key) const
reference container::front()
const_reference container::front()
const
reference container::back()
const_reference container::back()
const
Bedeutung
Liefert das Element mit dem Inhalt idx
Vorhanden bei: Vektoren, Deques, Strings
Liefert das Element mit dem Inhalt idx, im
Gegensatz zu at() finder keine Überprüfung von
idx statt. Der Programmierer muß sicherstellen,
daß idx einen erlaubten Wert besitzt
Vorhanden bei: Vektoren, Deques, Strings
Liefert das Element mit dem Schlüsselwert key
zum Abfragen und Setzen. Existiert kein Element
mit solchem Schlüsselwert, wird automatisch
eines angelegt und mit dem Default-Konstruktor
des Werte-Typs initialisiert. T ist der datentyp der
zum Schlüssel gehörenden Werte
Vorhanden bei: Maps
Liefert das erste Element (mit dem Index 0).
Der Aufrufer muß sicherstellen, daß ein solches
Element existiert (size() > 0).
Vorhanden bei: Vektoren, Deques, Listen
Liefert das letzte Element (das Element mit dem
Index size()-1)
Vorhanden bei: Vektoren, Deques, Listen
Iterator-Funktionen
Methode
iterator container::begin()
const_iterator container::begin()
const
Bedeutung
Liefert einen Iterator für den Anfang des
Containers (die Position des ersten elements im
Container). Ist der Container leer, entspricht dies
container::end()
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
iterator container::end()
Liefert einen Iterator für das Ende des Containers
const_iterator container::end() const (die Position hinter dem letzten Element im
Container)
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
reverse_iterator container::rbegin() Liefert einen Reverse-Iterator für den Anfang
const_reverse_iterator container::
eines umgekehrten Durchlaufs durch den
rbegin() const
Container
Vorhanden bei: Vektoren, Deques, Listen, Sets,
279
Die Programmiersprache C++
reverse_iterator container::rend()
const_reverse_iterator
container::rend() const
Multisets, Maps, Multimaps, Strings
Liefert einen Reverse-Iterator für das Ende eines
umgekehrten Durchlaufs durch den Container
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
Einfügen und Löschen
Methode
iterator container::insert(iterator
pos)
pair<iterator,bool>
container::insert(const T& wert)
iterator container::insert(const T&
wert)
iterator container::insert(iterator
pos, const T& wert)
Bedeutung
Fügt an die Position des Iterators pos ein Element
ein, das mit dessen Default-Konstruktor erzeugt
wird, liefert die Position des neuen Elements.
Vorhanden bei: Vektoren, Deques, Listen, strings
Fügt in einen assoziativen Container eine Kopie
von wert als Element ein
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Fügt an die Position des Iterators pos eine Kopie
von wert als Element ein, liefert die Position des
neuen Elements
void container::insert(iterator pos, Fügt an die Position des iterators pos anz Kopien
size_type anz, const T& wert)
von wert als Element ein.
T ist der Datentyp der Elemente, bei Vektoren und
Deques können dadurch Verweise auf andere
Elemente ungültig werden.
Vorhanden bei: Sets, Multisets, Maps, Multimaps
void container::insert(InputIterator Fügt in assoziative Container Kopien aller
anf, InputIterator end)
Elemente des Bereichs von anf bis end ein.
Vorhanden
bei:
Sets,
Multisets,
Maps, Multimaps
void container::insert(iterator pos, Fügt an die Position des Iterators pos Kopien
InputIterator anf, InputIterator end) aller Elemente des Bereichs von anf bis end ein.
Vorhanden bei: Vektoren, Deques, Listen, Strings
void container::push_front(const T&
Fügt als erstes Element eine Kopie von wert ein.
wert)
T ist der Datentyp der Elemente.
Vorhanden bei: Deques, Listen
void container::push_back(const T&
Fügt als letztes Element eine Kopie von wert ein.
wert)
T ist der Datentyp der Elemente.
Vorhanden bei: Vektoren, Deques, Listen
size_type container::erase(const T&
Entfernt bei assoziativen Containern die Elemente
wert)
mit dem Wert wert. Für das entfernte Element
wird dessen Destruktor aufgerufen.
Vorhanden bei: Sets, Multisets, Maps, Multimaps.
iterator container::erase(iterator
Entfernt das Element an der Position des Iterators
pos)
pos, liefert die Position des ursprünglichen
Nachfolgers des entfernten Elements bzw. end()
Für das entfernte Element wird dessen Destruktor
aufgerufen.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
iterator container::erase(iterator
Löscht die Elemente im bereich [anf,end), liefert
anf, iterator end)
die Position des ursprünglichen Nachfolgers des
letzten Elements bzw. end().
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
void container::pop_front()
Entfernt das erste Element im Container.
Vorhanden bei: Vektoren, Deques, Listen
void container:: pop_back()
Entfernt das letzte Element im Container.
Vorhanden bei: Vektoren, Deques, Listen
280
Die Programmiersprache C++
void container::resize(size_type anz) Ändert die Anzahl der Elemente im Container auf
anz, wächst dadurch die Größe werden
zusätzliche Elemente mit dem Default-Konstruktor
erzeugt und am Ende angefügt. Schrumpft
dadurch die Größe, werden die hinteren Elemente
zerstört (mit Aufruf von deren Destruktoren).
Vorhanden bei: Vektoren, Deques, Listen
void container::resize(size_type anz, Ändert die Anzahl der Elemente im Container auf
T& wert)
anz. Wächst dadurch die Größe, werden
zusätzliche Elemente als Kopie von wert erzeugt
und am Ende eingefügt.
Vorhanden bei: Vektoren, Deques, Listen
void container::clear()
Leert den Container (löscht alle Elemente im
Container), für die zerstörten elemente werden
deren Konstruktoren aufgerufen, dadurch werden
alle Verweise auf die Elemente ungültig.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
5.2.1 Bitset
Der Header <bitset> definiert eine Template-Klasse und zugehörige Funktionen
zur Darstellung und Bearbeitung von Bitfolgen fester Größe
template <size_t N> class bitset;
Die "bitset"-Klasse unterstützt Operationen mit Mengen vin Bits, z.B.
reset(), set(), size(), to_string().
flip(),
5.2.2 Deque
Der Name „Deque“ ist die Abkürung für double ended queue. Darunter versteht
man eine Warteschlange, die das Hinzufügen und Entnehmen von Elementen
sowohl am Anfang als auch am Ende erlaubt.
template <class T> class deque;
Datentyp
pointer
const_pointer
reverse_iterator
const
reverse_iterator
Bedeutung
Zeiger auf Deque-Element
Zeiger auf konstantes Deque-Element
Iterator für Durchlauf vom Ende zum Anfang
Iterator für Durchlauf vom Ende zum Anfang mit konstanten
Elementen
Abb.: Zusätzliche öffentliche Datentypen für deque
Die folgende Abbildung zeigt die Methoden einer Deque:
Methode mit Rückgabetyp
deque<T>()
deque<T>(const deque<T>&)
Bedeutung
Konstruktor
Kopierkonstruktor
281
Die Programmiersprache C++
~deque<T>()
iterator begin()
const_iterator begin()
iterator end()
const_iterator end()
size_type max_size()
size_type size()
bool empty()
void swap(deque<T>&)
operator=(const deque<T>&)
bool operator==(const deque<T>&)
bool operator!=(const deque<T>&)
bool operator<(const deque<T>&)
bool operator>(const deque<T>&)
bool operator<=(const deque<T>&)
bool operator>=(const deque<T>&)
Destruktor
Anfang des Containers
Anfang des Containers mit konstanten Elementen
Position nach dem letzen Element
end() für Container mit konstanten Elementen
Maximal mögliche Größe des Containers
Aktuelle Größe des Containers
size==0 bzw. begin() == end()
Vertauschen mit Argument-Container
Zuweisungsoperator
Operator ==
Operator !=
Operator <
Operator >
Operator <=
Operator <=
Abb.: Methoden der Klasse deque
5.2.3 List
Die Liste dieses Abschnitts ist eine doppelt-verkettete Liste, die das Hinzufügen und
Entnehmen von Elementen sowohl am Anfang als auch am Ende erlaubt. Jedes
Element in der Liste zeigt auf seinen Vorgänger und seinen Nachfolger. Dadurch ist
kein wahlfreier Zugriff mehr möglich.
template <class T> class list;
Datentyp
pointer
const_pointer
reverse_iterator
const
reverse_iterator
Bedeutung
Zeiger auf Listen-Element
Zeiger auf konstantes Listen-Element
Iterator für Durchlauf vom Ende zum Anfang
Iterator für Durchlauf vom Ende zum Anfang mit konstanten
Elementen
Abb. Zusätzliche öffentliche Datentypen für list
Die Klasse list stellt die folgende, spezielle Methoden zur Verfügung:
Methode mit Rückgabetyp
Bedeutung
void liste::unique()
Kollabiert Folgen von gleichen Elementen zu
void liste:.unique(BinaryBoolFunc op) einem Element, entfernt aus einer Liste jeweils
alle Elemente, die einem Element mit gleichem
Wert unmittelbar folgen bzw. bei denen
op(elem,folgeElem) wahr ist. Für entfernte
Elemente werden deren Destruktoren aufgerufen.
Diese Funktion ist die für Listen optimierte Version
des gleichnamigen Algorithmus.
void liste::splice(iterator pos,
Verschiebt alle Elemente aus c in die Liste, für die
liste& c)
die Funktion aufgerufen wird, vor das Element mit
Position pos. C wird dadurch geleert. Der
Programmierer muß sicherstellen, daß es sich bei
c um einen anderen Container handelt.
void liste::splice(iterator pos,
Verschiebt das Element aus c an der Position
liste& c, iterator cPos)
cPos in die Liste, für die die Funktion aufgerufen
282
Die Programmiersprache C++
void liste::splice(iterator pos,
liste& c, iterator cAnf, iterator
cEnd)
void liste:.sort()
void liste::sort(CompFunc op)
void liste::merge(liste& c)
void liste::merge(liste& c, CompFunc
op)
void liste::reverse()
wird, vor das Element mit der Position pos.
Falls c ein anderer Container ist, wird c dadurch
ein Element kleiner.
Verschiebt
alle
Elemente
des
Bereichs
[cAnf,cEnd) aus c in die Liste für die die
Funktion aufgerufen wird, vor das Element mit
Position pos. Bei c darf es sich um den gleichen
Container handeln. In diesem Fall darf pos nicht
im verschobenen Bereich liegen, und der Bereich
wird innerhalb des Containers verschoben. Falls c
ein anderer Container ist, wird c dadurch
entsprechend kleiner. Der Programmierer muß
sicherstellen, daß sich der verschobene Bereich
wirklich in c befindet. ansonsten ist das Verhalten
undefiniert.
Sortiert alle Elemente anhand < bzw. op. Op kann
optional zum Sortierem übergeben werden und
dient dazu, jeweils zwei Elemente zu vergleichen:
op(elem,elem).
Diese Fuktion ist die für Listen optimierte Version
der Algorithmen sort() und stable_sort().
Verschiebt alle Elemente aus der vorsortierten
Liste c in den vorsortierten Container, für den die
Funktion aufgerufen wird, so daß die gemeinsame
Menge ebenfalls sortiert ist.
Op kann optional übergeben werden und dient
dazu, jeweils zwei Elemente zu vergleichen:
op(elem,cElem), c wird dadurch geleert.
Diese Funktion ist die für Listen optimierte Version
des gleichnamigen Algorithmus.
Kehrt die Reihenfolge der Elemente in einer Liste
um. Diese Funktion ist die für Listen optimierte
Funktion des Algorithmus reverse().
Abb.: Spezielle Methoden der Klasse list
Bsp162.: Demonstration Listen-Container
// ANSI C Headers
#include <stdlib.h>
// C++ STL Headers
#include <algorithm>
#include <iostream>
#include <list>
#include <string>
int main(int argc, char *argv[])
{
string namen[] = { "Juergen", "Robert", "Philomena", "Ernst", "Andreas",
"Liesel", "Christian" };
const int N = sizeof(namen)/sizeof(namen[0]);
list< string > yrl;
list< string >::iterator iter;
for ( int i = 0; i < N; ++i) yrl.push_back(namen[i]);
for ( iter = yrl.begin(); iter != yrl.end(); ++iter )
cout << *iter << endl;
// Find "Ernst"
cout << "\nSiehe nach Ernst" << endl;
162
PR52301
283
Die Programmiersprache C++
iter = find( yrl.begin(), yrl.end(), "Ernst" );
// Maria soll vor Ernst stehen (Einfuegen davor)
if ( iter != yrl.end() )
{
cout << "\nFuege Maria vor Ernst ein" << endl;
yrl.insert( iter, "Maria" );
} else
{ cout << "\nKann Ernst nicht bestimmen" << endl; }
for ( iter = yrl.begin(); iter != yrl.end(); ++iter )
cout << *iter << endl;
return( EXIT_SUCCESS );
}
5.2.4 Map
Die Klasse map<Key, T>, eingebunden durch den Header <map>, speichert Paare
von Schlüsseln und zugehörigen Daten. Der Schlüssel ist eindeutig, es kann also
keine zwei Datensätze zu demselben Schlüssel geben.
template <class Key,
class T,
class Compare = less<Key>>
// Schluessel
// Daten
/* Standardvergleich für
Sortierung */
class map;
map ist ein assoziativer Container: die Daten werden durch direkte Angabe des
Schlüssels gefunden. Die Schlüssel liegen sortiert vor. Default-Wert ist das
Funktionsobjekt less, d.h. Elemente werden, soweit nicht anders vorgegeben,
aufsteigend sortiert.
Der Typ eines map-Elements ist pair<const Key,T>.
Im Gegensatz zu Maps erlauben Multimaps keine Duplikate:
template <class Key,
class T,
class Compare = less<Key>
// Schluessel
// Daten
/* Standardvergleich für
Sortierung */
class Allocator = allocator<T> >
class map;
Die Elemente einer Map oder Multimap können Wertepaare von zwei beliebigen
Typen Key und T sein.
Map und Multimap sind wie alle assoziativen Container-Klassen als balancierte
Bäume implementiert. Wegen der automatischen Sortierung und der
Dateiorganisation ergibt sich ein gutes Zeitverhalten beim Suchen und Finden von
Elementen.
Eine „map“ besitzt zusätzlich folgende Datentypen
Datentyp
pointer
const_pointer
reverse_iterator
const_reverse_iterator
key_type
value-type
mapped-type
Bedeutung
Zeiger auf Map-element
Zeiger auf konstantes Met-Element
Iterator für Durchlauf vom Ende zum Anfang
Iterator für Durchlauf vom Ende zum Anfang mit konstanten
Elementen
Entspricht Key
Entspricht pair<const key, T>
Entspricht T
284
Die Programmiersprache C++
key_compare
value_compare
Compare
Klasse für Funktionsobjekte, vgl value_comp()
bzw. Methoden163:
Methode mit Rückgabetyp
size_type container::count(const T&
wert) const
iterator container::find(const T&
wert)
const_iterator container::find(const
T& wert) const
iterator container::lower_bound(const
T& wert)
const_iterator
container::lower_bound(const T& wert)
const
iterator container::upper_bound(const
T& wert)
const_iterator
container::upper_bound(const T& wert)
const
pair<iterator,iterator>
container::equal_range(const T& wert)
pair<const_iterator,const_iterator>
container::equal_range(const T& wert)
const
value_compare container::value_comp()
163
Bedeutung
Liefert die Anzahl der Elemente, die den Wert
wert besitzen.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, maps, Multimaps
Liefert die Position vom ersten Element, das einen
Wert gleich wert besitzt. Kann kein passendes
Element
gefunden
werden,
wird
end()
zurückgeliefert. Die Elementfunktion ist eine
Spezialversion des gleichnamigen Algorithmus.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert die erste Position, an der wert eingefügt
werden kann, ohne die Sortierung zu zerstören.
Dies ist gleichbedeutend mit dem ersten Element,
dessen Wert >= wert ist. Die Elementfunktion ist
eine
Spezialversion
des
gleichnamigen
Algorithmus.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert die letzte Position, an der wert eingefügt
werden kann, ohne die Sortierung zu zerstören.
Dies ist gleichbedeutend mit dem ersten Element,
dessen Wert > wert ist. Die Elementfunktion ist
eine
Spezialversion
des
gleichnamigen
Algorithmus.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert ein Wertepaar mit der ersten und letzten
Position, an der wert eingefügt werden kann,
ohne die Sortierung zu zerstören. Dies ist
gleichbedeutend mit dem Bereich der elemente,
deren Wert == wert ist. Die Elementfunktion ist
eine
Spezialversion
des
gleichnamigen
Algorithmus.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert
die
Vergleichsfunktion
bzw.
das
Elementfunktionen, die spezielle Implementierungen von Algorithmen für assoziative Container sind.
285
Die Programmiersprache C++
key_compare container::key_comp()
Vergleichsobjekt, mit dem die Elemente verglichen
werden.
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert
die
Vergleichsfunktion
bzw.
das
Vergleichsobjekt, mit dem Schlüssel verglichen
werden.
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Bsp.: Zählen von Worten in einer Textdatei164
#include
#include
#include
#include
<string>
<map>
<fstream>
<assert.h>
using namespace std;
class Zaehler
{
private:
int i;
public:
Zaehler() : i(0) { }
void operator++(int)
{
i++;
}
int wert() { return i; }
};
typedef map<string, Zaehler, less<string> > wortmap;
const char* begrenzer = "\t.,:;\"{}-+&^%$#@!~'\\<|()[]<>*=";
int main(int argc, char* argv[])
{
assert(argc == 2);
ifstream ein(argv[1]);
assert(ein);
wortmap worte;
const int gr = 255;
char puffer[gr];
while (ein.getline(puffer,gr))
{
char* wort = strtok(puffer,begrenzer);
while (wort)
{
worte[string(wort)]++;
wort = strtok(0,begrenzer);
}
}
for (wortmap::iterator w = worte.begin();
w != worte.end(); w++)
cout << (*w).first << ": " << (*w).second.wert()
<< endl;
}
164
vgl. PR52410.CPP
286
Die Programmiersprache C++
5.2.5 Queue
Eine Queue oder Warteschlange erlaubt die Ablage von Objekten auf einer Seite
und ihre Entnahme von der anderen Seite. Sowohl list als auch deque sind
geeignete Elemente zur Implementierung165.
queue<double> schlange_1;
// mit deque realisierte Queue
queue<double,list<double> schlange_2; // mit list realisierte Queue
Die Deklaration der Klasse ist:
template <class T, class Container = deque<T>> class queue;
Eine Queue hat zusätzlich bestimmte, öffentliche Datentypen:
Datentyp
value_type
size_type
Bedeutung
Typ der Element
Integraler Typ ohne Vorzeichenangabe für Größenangaben
container_type
Typ des Containers
Abb. Zusätzliche Datentypen für queue
Eine Queue stellt folgende Methoden zur Verfügung:
Methode mit Rückgabetyp
Bedeutung
queue(const Container& = Container()) Konstruktor. Eine Queue kann mit einem bereits
vorhandenen Container initialisiert werden.
Container ist Typ des Containers.
bool empty() const
Gibt an, ob der Queue leer ist
size_type size() const
Gibt die Anzahl der in der Queue befindlichen
Elemente zurück
const value_type& front() const und
Gibt das erste Element zurück
value_type& front()
const value_type& back() const und
Gibt das letzte Element zurück
value_type& back()
void push(const value_type& x)
Legt das Element x am Ende der Queue ab.
void pop()
entfernt das erste Element der Queue.
Eine Priority-Queue ist eine prioritätsgesteuerte Warteschlange. Jedem Element
wird eine Priorität zugeordnet, die den Platz innerhalb der Priority-Queue schon beim
Einfügen bestimmt. Die relative Priorität wird durch Vergleich jeweils zweier
Elemente bestimmt, indem entweder der „<“-Operator oder wahlweise ein
Funktionsobjekt herangezogen wird.
Die Deklaration der Klasse ist:
template <class T, class Container = deque<T>,
class Compare = less<typname Container::value_type>>
class priority_queue;
Eine Priority-Queue stellt diesselben Datentypen wie die Queue und die Folgenden
Methoden zur Verfügung:
Methode mit Rückgabetyp
priority_queue(const Compare& cmp =
165
Bedeutung
Konstruktor. Eine Priority-Queue kann mit einem
Falls nichts anderes angegeben wird, wird eine deque verwendet.
287
Die Programmiersprache C++
Compare(), const Container& =
Container())
bool empty() const
size_type size() const
const value_type top()const
void push(const value_type& x)
void pop()
bereits vorhandenen Container initialisiert werden.
Container ist Typ des Containers. Cmp ist das
Funktionsobjekt, mit dem verglichen wird. Falls
kein Funktionsobjekt angegeben wird, wird
less<container::value_type>
angenommen.
Gibt an, ob der Priority-Queue leer ist
Gibt die Anzahl der in der Priority-Queue
befindlichen Elemente zurück
Gibt das erste Element zurück, d.h. das mit der
größten Priorität.
Fügt das Element x ein.
entfernt das erste Element der Priority-Queue.
5.2.6 Set
Die Klasse set<Key, T> für Mengen, eingebunden durch den Header <set>,
entspricht der Klasse map. Es werden aber nur Schlüssel gespeichert.
template <class Key, // Schluessel
class Compare = less<Key> >
/* Standardvergleich für Sortierung */
class set;
set ist ein assoziativer Container, die Schlüssel liegen sortiert vor. Falls eine
Sortierung nicht mit dem Operator „<“ (less<Key>) hergestellt werden soll, kann
eine Klasse für Funktionsobjekte angegeben werden.
Ein Multiset kennt im Gegensatz zum Set keine Duplikate
template <class Key, // Schluessel
class Compare = less<Key>
// Standardvergleich für
//
Sortierung
class Allocator = allocator<T> >
class set;
Die Elemente eines Set oder Multiset können von einem beliebigen Typ T sein. T
muß sortierbar sein, d.h. der Operator < muß definiert sein. Der zweite optionale
Parameter legt das Default-Sortierkriterium fest. Der Default-Wert, das
Funktionsobjekt less bedeutet: Elemente, sofern nicht anders angegeben, werden
aufsteigend sortiert.
Die beiden Container-Klassen sind als balancierte Binärbäume166 implementiert.
Ein „set“ besitzt zusätzlich folgende Datentypen
Datentyp
pointer
const_pointer
reverse_iterator
const_reverse_iterator
key_type
value-type
key_compare
Bedeutung
Zeiger auf Set-element
Zeiger auf konstantes Set-Element
Iterator für Durchlauf vom Ende zum Anfang
Iterator für Durchlauf vom Ende zum Anfang mit konstanten
Elementen
Entspricht Key
Entspricht Key
Compare
166
Der Standard legt nicht direkt fest, wie assoziative Container implementiert sind. Vielfach werden diese
Bäume als sog. „Red-Black-Trees“ implementiert.
288
Die Programmiersprache C++
value_compare
Compare
bzw. Methoden:
Methode mit Rückgabetyp
set(const Compare& cmp = Compare())
pair<iterator, bool> insert(x)
size_type erase(k)
void erase(p, q)
void clear()
value_compare value_comp() const
Bedeutung
Konstruktor, der ein Compare-Objekt akzeptieren
kann. Falls keins angegeben wird, wird
less<Key> angenommen.
Fügt den Schlüssel x ein, sofern er noch nicht
vorhanden ist. Der Iterator des zurückgegebenen
Paares zeigt auf den eingefügten Schlüssel bzw.
auf den schon vorhandenen Schlüssel mit
demselben Wert wie x. Der Warheitswert zeigt an,
ob überhaupt ein Einfügen stattgefunden hat
Löscht das Element, auf das der Iterator zeigt
Alle Elemente im Iteratorbereich p bis q löschen
Löscht alle Elemente
(dasselbe wie key_comp().
Bsp.: Sortieren der Wörter einer Textdatei167
#include
#include
#include
#include
<string>
<set>
<fstream>
<assert.h>
using namespace std;
const char* begrenzer = " \t;()\"<>:{}[]+-=&*#.,/\\" "0123456789";
int main(int argc, char* argv[])
{
assert(argc == 2);
ifstream ein(argv[1]);
assert(ein);
set<string, less<string> > woerter;
const int gr = 1024;
char puffer[gr];
while (ein.getline(puffer,gr))
{
char* s = strtok(puffer,begrenzer);
while (s)
{
woerter.insert(s);
s = strtok(0, begrenzer);
}
}
copy(woerter.begin(), woerter.end(),
ostream_iterator<string>(cout, "\n"));
}
167
pr52611.CPP
289
Die Programmiersprache C++
5.2.7 Stack
Ein Stack erlaubt die Ablage und Entnahme nur auf einer Seite.
template <class T, class Container = deque<T>> class stack;
Ein Stack benutzt intern einen anderen Container, der die Operationen back(),
push_back() und pop_back() unterstützt. Falls nichts anderes angegeben wird, wird
eine Deque verwendet:
stack <double> stapel_1;
stack<double, vector<double>> stapel_2;
// mit deque realisierter Stack
// mit vector realisierter Stack
Ein Stack hat zusätzlich bestimmte, öffentliche Datentypen:
Datentyp
value_type
size_type
container_type
Bedeutung
Typ der Elemente
Integraler Typ ohne Vorzeichen für Größenangaben
Typ des Containers
Abb.: Stack-Datentypen
Die Klasse stack stellt die folgenden Methoden zur Verfügung:
Methode mit Rückgabetyp
Bedeutung
stack(const Container& = Container()) Konstruktor. Ein Stack kann mit einem bereits
vorhandenen Container initialisiert werden.
Container ist Typ des Containers.
bool empty() const
Gibt an, ob der Stack leer ist
size_type size() const
Gibt die Anzahl der im Stack befindlichen
Elemente zurück
size_type size() const
Gibt die Anzahl der im Stack befindlichen
Elemente zurück
const value_type& top() const und
Geben das oberste Element zurück
value_type& top()
void push(const value_type& x)
Legt das Element x auf dem Stack ab.
void pop()
entfernt das oberste Element vom Stack.
Für die Stack-Klasse gibt es außerdem die globalen relationalen Operatoren:
template <class T, class Container>
bool operator==(const stack<T, Container>& x,
const stack<T, Container>& y);
template <class T, class Container>
bool operator<(const stack<T, Container>& x,
const stack<T, Container>& y);
template <class T, class Container>
bool operator>(const stack<T, Container>& x,
const stack<T, Container>& y);
template <class T, class Container>
bool operator!=(const stack<T, Container>& x,
const stack<T, Container>& y);
template <class T, class Container>
bool operator>=(const stack<T, Container>& x,
const stack<T, Container>& y);
template <class T, class Container>
bool operator<=(const stack<T, Container>& x,
const stack<T, Container>& y);
290
Die Programmiersprache C++
Bsp.: „Umrechnen von Dezimalzahlen in andere Basisdarstellungen“168
#include <iostream>
#include <vector>
#include <stack>
// Ein
Testprogramm
void ausgabe(long zahl, int b)
{
stack<int, vector<int> > s;
// Extrahiere die Ziffern zur jeweiligen Basis (von rechts nach links)
// und lege sie im Stapel ab
do
{
s.push(zahl % b);
zahl /= b;
}
while (zahl != 0);
while (!s.empty())
{
cout << s.top();
s.pop();
}
}
int main(void)
{
long zahl;
//
int b;
// Basis
// lies 3 positive ganze Zahlen und die gewuenschte Basis
for (int i = 0; i < 3; i++)
{
cout << "\nGib eine nicht negative ganze Dezimalzahl und "
<< "\ndanach die Basis (2 <= b <= 9) an" << endl;
cin >> zahl >> b;
cout << zahl << " basis " << b << " ist: ";
ausgabe(zahl,b);
cout << endl;
}
}
5.2.8 Vector
Die Deklaration der Klasse ist: template<class T> class vector
Ein vector-Container verwaltet die Elemente in einem dynamischen Array. Er
ermöglicht wahlfreien Zugriff (random access). Auf jedes Element kann mit einem
entsprechenden Index direkt zugegriffen werden. Das Anhängen und Löschen von
Elementen am Ende des Array geht optional schnell. Das Einfügen und Löschen von
Elementen mitten im Array kostet Zeit, da dann alle Elemente entsprechend
verschoben werden müßten.
Ein Vektor hat zusätzlich bestimmte, öffentliche Datentypen:
Datentyp
pointer
const_pointer
168
Bedeutung
Zeiger auf Vektor-Element
Zeiger auf konstantes Vektor-Element
pr52702.CPP
291
Die Programmiersprache C++
reverse_iterator
const
reverse_iterator
Iterator für Durchlauf vom Ende zum Anfang
Iterator für Durchlauf vom Ende zum Anfang mit konstanten
Elementen
Abb. Zusätzliche Datentypen für vector
Die Klasse vector stellt die folgenden Methoden zur Verfügung:
Methode mit Rückgabetyp
vector<T>()
vector<T>(const vector<T>&)
~vector<T>()
iterator begin()
const_iterator begin()
iterator end()
const_iterator end()
size_type max_size()
size_type size()
bool empty()
void swap(vector<T>&)
operator=(const vector<T>&)
bool operator==(const vector<T>&)
bool operator!=(const vector<T>&)
bool operator<(const vector<T>&)
bool operator>(const vector<T>&)
bool operator<=(const vector<T>&)
bool operator>=(const vector<T>&)
vector(size_type n,const T& t = T())
template <class InputIterator>
vector(InputIterator i, InputIterator
j)
void push_back(const T& t)
void pop_back()
iterator insert(iterator p, const T&
t)
void insert(iterator p,size_type n,
const T& t)
iterator erase(iterator q)
iterator erase(iterator q1, iterator
q2)
void clear()
void reserve(size_type n)
size_type capacity() const
Bedeutung
Konstruktor
Kopierkonstruktor
Destruktor
Anfang des Containers
Anfang des Containers mit konstanten Elementen
Position nach dem letzen Element
end() für Container mit konstanten Elementen
Maximal mögliche Größe des Containers
Aktuelle Größe des Containers
size==0 bzw. begin() == end()
Vertauschen mit Argument-Container
Zuweisungsoperator
Operator ==
Operator !=
Operator <
Operator >
Operator <=
Operator <=
Erzeugt einen Vektor mit n Kopien von t.
Erzeugt einen Vektor, wobei Elemente i .. j in
den Vektor kopiert werden.
Fügt t am Ende ein.
Löscht das letzte Element
Fügt eine Kopie von t vor die Stelle p ein. Der
Rückgabetyp zeigt auf die eingefügte Kopie.
Fügt n Kopien von t vor die Stelle t ein
Löscht das Element, auf das q zeigt.
Löscht die Elemente im Bereich q1 .. q2. Der
zurückgegebene Iterator zeigt auf das Element,
das q vor dem Löschvorgang unmittelbar folgt,
sofern es existiert. Anderenfalls wird end()
zurückgegeben.
Löscht alle Elemente
Speicherpaltz reservieren, so daß der verfügbare
Platz größer als der aktuelle ist. Zweck:
Vermeidung
von
Speicherplatzbeschaffungsoperatoren während der Benutzung des Vektors
Gibt den Wert der Kapazitärt zurück. size() ist
immer kleiner oder gleich als capacity()
Abb.: Methoden der Klasse vector
292
Die Programmiersprache C++
5.3 Iteratoren
Iteratoren sind Objekte, die über Container (Iteratoren) wandern können. Jedes
Iterator-Objekt repräsentiert eine Position in einem Container. Drei fundamentale
Operatoren definieren das Verhalten eines Iterators:
iterator::operator*()
// liefert das Element, an dessen Position sich der Operator befindet. Sofern die Elemente Objekte
// sind, ist auch der Operator -> definiert. Er ermöglicht den direkten Zugriff auf eine Komponente.
iterator::operator++()
// setzt den Iterator ein Element weiter.
iterator::operator==()
// zeigt, ob zwei Iteratoren das gleiche Objekt repräsentieren.
Diese Schnittstelle entspricht genau der Art und Weise, wie in C/C++ Zeiger über
Arrays wandern können. Iteratoren können aber mit beliebigen Containern umgehen:
Die Container stellen die dazugehörigen Iteratorklassen zur Verfügung und
implementieren damit auch deren Operatoren. Das bedeutet: Iteratoren von
verschiedenen Containern können unterschiedliche Typen besitzen.
In der C++-Standardbibliothek wird ein Standardtyp für Iteratoren angegeben, von
dem jeder benutzerdefinierte Iterator erben kann:
namespace std
{
template <class Category, class T, class Distance = ptrdiff_t,
class Pointer = T*, class Reference = T&>
struct iterator
{
typedef Distance difference_type;
typedef T value_type;
typedef Pointer pointer;
typedef Reference reference;
typedef Category iterator_category;
}
};
Über public-Vererbung sind die Namen in allen abgeleiteten Klassen sichtbar und
verwendbar.
Iterator-spezifische Datentypen und Funktionen werden in der Header-Datei
<iterator> definiert. Diese Datei muß aber nicht eingebunden werden, da sie von
allen Containern und der Header-Datei für Algorithmen ohnehin eingebunden wird.
Iteratoren spezifizieren eine Position in einem Container. Sie können inkrementiert,
dekrementiert werden. Zwei Iteratoren können miteinander verglichen werden. Es
gibt einen speziellen Iterator-Wert „past-the-end“. Über die Nachricht „begin()“
kann ein Iterator auf das erste Element eines Containers gestzt werden. Mit der
Nachricht „end()“ wird ein „past-the-end“ Iterator erhalten, z.B.:
293
Die Programmiersprache C++
vector<int> v;
vector::iterator i1 = v.begin();
vector::iterator i2 = v.end();
i1
i2
v
<T>
<T>
<T>
<T>
<T>
<T>
<T>
*i1
Operationen (z.B. Sortieren) benutzen die beiden Iteratoren als Spezifikation des
(Quell-) Bereichs. Das (Quell-) Element wird durch Inkrementieren und
Dereferenzieren des ersten Iterators beschafft. Es stimmt dann mit dem zweiten
Iterator überein. Zwei Iteratoren sind gleich, wenn sie sich auf dasselbe Element
desselben Vektors beziehen.
Bsp.:
1. Das folgende Programm169 zeigt, wie in C/C++ eine Liste mit ganzen Zahlen
erstellt, sortiert und anschließend ausgegeben werden kann.
#include <stdlib.h>
#include <iostream.h>
inline int vrgl(const void* a, const void* b)
{
int aa = * (int*) a;
int bb = * (int*) b;
return (aa < bb) ? -1 : (aa > bb) ? 1 : 0;
}
int main()
{
int x[100];
int n = 0;
// Lies eine ganze Zahl in das n + 1. Element des Array
while (cin >> x[n++]);
n--;
qsort(x,n,sizeof(int),vrgl);
for (int i = 0; i < n; i++)
cout << x[i] << endl;
}
2. Das folgende Beispiel170 zeigt, wie das Sortieren mit Random-Access-Iteratoren
erledigt werden kann.
#include
#include
#include
#include
#include
<string.h>
<algorithm>
<vector>
<stdlib.h>
<iostream.h>
int main()
{
vector<int> v;
int ein;
while (cin >> ein)
169
170
pr53010.CPP
pr53011.CPP
294
Die Programmiersprache C++
v.push_back(ein);
sort(v.begin(),v.end());
int n = v.size();
for (int i = 0; i < n; i++)
cout << v[i] << endl;
}
5.3.1 Iteratorkategorien
Es gibt verschiedene Kategorien von Iteratoren in einer hierarchischen Anordnung:
Input-Iterator
Dient zum sequentiellen Lesen von Daten, z.B. aus einem Container oder einer
Datei. Ein Zurück an eine gelesene Stelle ist nicht möglich („--“-Operator ist nicht
definiert).
Ausdruck
iter1==iter2
Iter1!=iter2
*iter
iter->komp
++iter
iter++
TYP(iter)
Bedeutung
Test auf Gleichheit mit anderem Iterator
Test auf Ungleichheit mit anderem Iterator
Lese-Zugriff auf das aktuelle Element des Iterators
Zugriff auf eine Komponente des aktuellen Iterators
Weitersetzen (liefert neue Position)
Weitersetzen (liefert alte Position)
Kopieren (Copy-Konstruktor)
Abb.: Operationen für Input-Iteratoren
Output-Iterator
Kann sequentiell in einen Container oder in eine Datei schreiben, wobei der
Dereferenzierungsoperator verwendet wird.
Ausdruck
*iter = wert
++iter
iter++
TYP(iter)
Bedeutung
Schreib-Zugriff auf das aktuelle Element eines Iterators
Weitersetzen (liefert neue Position)
Weitersetzen (liefert alte Position)
Kopieren (Copy-Konstruktor)
Abb. Operationen für Output-Iteratoren
Forward-Iterator
Kann sich vorwärts bewegen. Im Unterschied zu den vorgenannten Iteratoren
können jedoch Werte des Iterators gespeichert werden, z. B. um ein Element des
Containers wiederzufinden. Damit ist ein mehrfacher Durchlauf in eine Richtung
möglich, z.B. durch eine einfach verkettete Liste, falls man sich den Anfang gemerkt
hat.
Ausdruck
iter1==iter2
iter1!=iter2
*iter
iter->komp
++iter
iter++
TYP()
TYP(iter)
Bedeutung
Test auf Gleichheit mit anderem Iterator
Test auf Ungleichheit mit anderem Iterator
Lese-Zugriff auf das aktuelle Element des Iterators
Zugriff auf eine Komponente des aktuellen Iterators
Weitersetzen (liefert neue Position)
Weitersetzen (liefert alte Position)
Erzeugen (Default-Konstruktor)
Kopieren (Copy-Konstruktor)
295
Die Programmiersprache C++
iter1=iter2
Zuweisung
Abb.: Operationen für Forward-Iteratoren
Bidirectional-Iterator
Kann alles, was ein Forward-Iterator kann. Zusätzlich kann er noch mit dem „--„Operator rückwärts gehen, so daß er bspw. für eine doppelt verkettete Liste geeignet
ist.
Ausdruck
--iter1
iter--
Bedeutung
Zurücksetzen (liefert neue Position)
Zurücksetzen (liefert alte Position)
Abb.: Zusätzliche Operationen für Bidirectional-Iteratoren
Random-Access-Iterator
Kann alles, was ein Bidirectional-Iterator kann. Zusätzlich ist wahlfreier zugriff
möglich, wie er für einen Vektor benötigt wird. Der wahlfreie Zugriff wird durch den
Indexoperator operator[]() realisiert.
Ausdruck
iter[n]
iter += n
iter -= n
iter + n
n + iter
iter - n
iter1 < iter2
iter1 > iter2
iter1 - iter2
iter1 <= iter2
iter1 >= iter2
Bedeutung
Zugriff auf das nte Element
Iterator n Elemente weitersetzen (bzw. bei negativem wert zurücksetzen)
Iterator n Elemente zurücksetzen (bzw. bei negativem Wert weitersetzen)
Iterator für das nte folgende Element liefern
Iterator für das nte folgende Element liefern
Iterator für das nte vorherige Element liefern
Zeigt, ob iter1 vor iter2 liegt
Zeigt, ob iter1 hinter iter2 liegt
Abstand zwischen iter1 und iter2 liefern
Zeigt, ob iter1 nicht hinter iter2 liegt
Zeigt, ob iter1 nicht vor iter2 liegt
Abb.: Zusätzliche Operationen für Random-Access-Iteratoren
5.3.2 distance(), advance() und iter_swap()
Mit distance() kann der Abstand zwischen zwei Iteratoren ermittelt werden, mit
advance() kann der Iterator eine bestimmte Anzahl von Elementen weitergesetzt
werden. Mit iter_swap() können die Werte der Positionen von zwei Iteratoren
vertauscht werden.
// schaltet i um n Positionen vor bzw. zurück, falls n > 0
template <class InputIterator, class Distance>
void advance(InputIterator& i, Distance n)
// gibt den Abstand zwischen zwei Iteratoren zurueck
// last muss von first aus erreichbar sein
template <class InputIterator>
typname iterator_traits<InputIterator>::difference_type
distance(InputIterator first, InputIterator last);
296
Die Programmiersprache C++
void iter_swap(Forward_Iterator1 pos1, Forward_Iterator2 pos2)
Die beiden Iteratoren müssen nicht den gleichen Typ besitzen. Die beiden Elemente
müssen gegenseitig zuweisbar sein.
5.3.3 Iterator-Adapter
Reverse-Iteratoren
Eine Reverse-Iterator ist bei einem bidirektionalen Iterator immer möglich. Ein
Reverse-Iterator durchläuft einen Container rückwärts mit der ++-Operation. Beginn
und Ende des Containers werden mit rbegin() und rend() markiert. Einige
Container stellen in Abhängigkeit von ihrem Typ Reverse-Iteratoren zur Verfügung.
Diese Iteratoren werden mit der vordefinierten Klasse
template <class Iterator> class reverse_iterator;
realisiert. Die Container-Funktionen rbegin() und rend() liefern jeweils einen
Reverse-Iterator:
-
rbegin() liefert die Position des ersten elements eines umgekehrten Durchlaufs, also das letzte
Element im Container.
rend() liefert die Position hinter dem letzten Element eines umgekehrten Durchlaufs, also die
Position vor dem ersten Element im Container
Bsp.:
Insert-Iteratoren
Insert-Iteratoren sind Iterator-Adapter, die Zuweisungen an den Iterator bzw. an das
Element des Iterators in ein Einfügen umsetzen.
Ausdruck
*iter
iter = wert
++iter
iter++
Bedeutung
Nichts (liefert iter zurück)
Fügt wert an iter-Position ein
Nichts (liefert iter zurück)
Nichts (liefert iter zurück)
Abb.: Operationen von insert-Operatoren
Es gibt drei Arten von Insert-Operatoren
Name
Back-Inserter
Front-Inserter
Inserter
Klasse
back-insert_iterator
front_insert_iterator
insert_iterator
Elementfunktion
push_back(wert)
push_front(wert)
insert(pos,wert)
Abb. Arten von Insert-Iteratoren
1. front_insert_iterator
297
Erzeugung
back_inserter(container)
front_inserter(container)
inserter(container,pos)
Die Programmiersprache C++
Dieser Insert-Iterator fügt etwas am Anfang des Containers ein. Der Container muß
die Methode push_front() zur Verfügung stellen.
2. back_insert_iterator
Dieser Insert-Iterator fügt etwas am Ende des Containers ein. Der Container muß die
Methode push_back() zur Verfügung stellen.
3. insert_iterator
Dieser Insert-Iterator fügt etwas an einer ausgewählten Position in den Container
ein. Der Container muß die Methode insert() zur Verfügung stellen. Dem InsertIterator muß die gewünschte Einfügeposition mitgegeben werden.
5.3.4 Stream-Iteratoren
Sie dienen zum sequentiellen Lesen und Schreiben mit den Operatoren << und >>.
Der Istream-Operator ist ein Input-Iterator, der Ostream-Iterator ist ein OutputIterator.
istream-Iterator
Ein Eingabestrom, z.B. cin, hat die richtige Funktionalität für einen Input-Iterator
(Zugriff auf eine Folge von Elementen). Allerdings erwarten Operationen mit
Iteratoren „Inkrementieren und Dereferenzieren“. Diese Fähigkeiten stellt ein „cin“
nicht bereit. Die STL sieht dafür Adaptoren vor: Typen die das Interface auf andere
Typen transformieren. Ein sehr nützlicher Adapter ist der „istream-iterator“, der
mit dem gewünschten Objekttyp, der vom Eingabestrom eingelesen werden soll,
parametrisiert ist. Eingabestrom-Iteratoren werden mit einem Strom initialisiert.
Danach kann über Dereferenzieren ein Element aus dem Strom gelesen werden. Ein
Eingabestrom-Iterator, der mit einem Default-Konstruktor erzeugt wurde, bekommt
den „past-the-end“-Wert zugewiesen, falls der zum Eingabestrom zugehörige
Iterator das Ende erreicht hat.
298
Die Programmiersprache C++
iter++
Algorithmen
cin >> v
Iter
cin
*iter
return v
Abb.:
Zum Einlesen der Elemente in einen Vektor dient der copy-Algorithmus der STL.
Dieser Algorithmus benutzt drei Iteratoren. Die ersten beiden spezifizieren den
Quellbereich, der dritte das Ziel, z.B.:
typedef istream_iterator<int,ptrdiff_t> istream_iterator_int;
// Kopieren von der Standard-Eingabe
copy(istream_iterator_int(cin), istream_iterator_int(),v.begin())
Ausdruck
istream_iterator<T>()
istream_iterator<T,ptrdiff_t>()
istream_iterator<T>(istream)
istream_iterator<T,ptrdiff_t>(istream)
*iter
iter->komp
++iter
iter++
iter == wert
iter != wert
Bedeutung
Erzeugt einen End-Of-Stream-Iterator
Erzeugt einen End-Of-Stream-Iterator
Erzeugt einen Istream-Iterator
Erzeugt einen Istream-Iterator
Liefert den zuletzt gelesenen Wert
Liefert eine Komponente vom zuletzt gelesenen wert
Liefert nächsten Wert und liefert Iterator zurück
Liefert nächsten Wert und liefert vorherigen Iterator zurück
Test der beiden Iteratoren auf Gleichheit
Test von beiden Iteratoren auf Ungleichheit
Abb.: Operationen von istream-Iteratoren
ostream_iterator
Auf ähnliche Weise werden die Werte (nach dem Sortieren) in den Ausgabestrom
(Standard-Ausgabe) geleitet.
Ausdruck
ostream_iterator<T>(ostream)
ostream_iterator<T>(ostream, zf)
*iter
iter = wert
++iter
iter++
Bedeutung
Erzeugt einen Ostream-Iterator
Erzeugt einen ostream-Iterator, der zf zwischen den Elementen
ausgibt
Nichts (liefert iter zurück)
Gibt wert auf ostream aus (ostream << wert)
Nichts (liefert iter zurück)
Nichts (liefert iter zurück)
Abb.: Operationen von ostream-Iteratoren
299
Die Programmiersprache C++
Bsp.171: „Sortieren der Standard-Eingabe mit anschließender Ausgabe auf die
Standard-Ausgabe“
#include
#include
#include
#include
#include
<string.h>
<algorithm>
<vector>
<stdlib.h>
<iostream.h>
int main()
{
vector<int> v;
istream_iterator<int,ptrdiff_t> start(cin);
istream_iterator<int,ptrdiff_t> end;
back_insert_iterator<vector<int> > ziel(v);
copy(start, end, ziel);
sort(v.begin(), v.end());
copy(v.begin(), v.end(), ostream_iterator<int>(cout,"\n"));
}
5.3.5 Iterator-Traits
Iterator-Tags
sind Datentypen, die einfach für eine entsprechende Iterator-Kategorie stehen.
Iterator-Traits
sind Strukturen, die sicherstellen, daß alle Iteratoren die gleichen datentypKomponenten besitzen.
171
pr53510.CPP
300
Die Programmiersprache C++
5.4 Algorithmen
Alle im Header <algorithm> vorhandenen Algorithmen sind unabhängig von der
speziellen Implementierung der Container, auf denen sie arbeiten. Sie kennen nur
Iteratoren, über die auf Datenstrukturen in Containern zugegriffen werden kann.
for_each
Der Algorithmus for_each bewirkt, daß auf jedem Element eines Containers eine
Funktion ausgeführt wird:
template <class InputIterator, class Function>
Function for_each(InputIterator first,
InputIterator last,
Function f)
„f“ kann sowohl eine Funktion als auch ein Funktionsobjekt sein und wird nach Gebrauch
zurückgegeben.
find und find_if
find() tritt in 2 Arten auf: mit oder ohne erforderliches Prädikat (find_if()):
template <class InputIterator, class T>
InputIterator find(InputIterator first,
InputIterator last,
const T& wert);
template <class InputIterator, class Predicate>
InputIterator find_if(InputIterator first, InputIterator last,
Predicate pred);
find_end
Der Algorithmus findet eine Subsequenz innerhalb einer Sequenz
find_first_of
Der Algorithmus findet eine Subseqenz innerhalb einer Sequenz
adjacent_find
Zwei gleiche direkt benachbarte Elemente werden mit der Funktion adjacent_find
gefunden.
template <class ForwardIterator>
ForwardIterator adjacent_find(ForwardIterator first,
ForwardIterator last);
301
Die Programmiersprache C++
template <class ForwardIterator, class BinaryPredicate>
ForwardIterator adjacent_find(ForwardIterator first,
ForwardIterator last,
BinaryPredicate binary_pred);
count
Der Algorithmus gibt die Anzahl zurück, wie viele Elemente gleich einem bestimmten
wert sind bzw. wie viele Elemente ein bestimmtes Prädikat erfüllen.
template <class InputIterator, class T>
iterator_traits<inputIterator>::difference_type count(InputIterator first,
InputIterator last,
const T& wert);
template <class InputIterator, class Predicate>
iterator_traits<inputIterator>::difference_type count(InputIterator first,
InputIterator last,
Predicate pred);
mismatch
überprüft zwei Container auf Übereinstimmung ihres Inhalts, wobei eine Varianle ein
binäres Prädikat benutzt.
equal
equal() überprüft zwei Container auf Übereinstimmung ihres Inhalts, wobei eine
Variante ein binäres Prädikat benutzt.
template <class InputIterator1, class InputIterator2>
bool equal(InputIterator1 first1,
InputIterator1 last1,
InputIterator2 first2);
template <class InputIterator1, class InputIterator2,class BinaryPredicate>
bool equal(InputIterator1 first1,
InputIterator1 last1,
InputIterator2 first2,
Predicate binary_pred);
302
Die Programmiersprache C++
search
Der Algorithmus durchsucht eine Sequenz, ob eine zweite Sequenz in ihr enthalten
ist. Es wird ein Iterator auf die Position innerhalb der ersten Sequenz
zurückgegeben, an der die zweite Sequenz beginnt, sofern sie in der ersten
enthalten ist. Andernfalls wird ein Iterator auf die last1-Position der ersten Sequenz
zurückgegeben
template <class ForwardIterator1, class ForwardIterator2>
ForwardIterator1 search(ForwardIterator1 first1,
ForwardIterator1 last1,
ForwardIterator1 first2,
ForwardIterator2 last2);
template <class ForwardIterator1, class ForwardIterator2,
class BinaryPredicate>
ForwardIterator1 search(ForwardIterator1 first1,
ForwardIterator1 last1,
ForwardIterator1 first2,
ForwardIterator2 last2,
BinaryPredicate binary_pred);
search_n
search_n() durchsucht eine Sequenz daraufhin, ob eine Folge von gleichen
Werten in ihr enthalten ist.
template <class ForwardIterator, class Size, class T>
ForwardIterator search-n(ForwardIterator first,
ForwardIterator last,
Size count,
const T& value);
template <class ForwardIterator, class Size, class T,
class BinaryPredicate>
ForwardIterator search_n(ForwardIterator first,
ForwardIterator last,
Size count,
const T& value,
BinaryPredicate binary_pred);
copy und copy_backward
copy() kopiert die Elemente eines Quellbereichs in den Zielbereich, das Kopieren
beginnt am Anfang bzw. am Ende (mit copy_backward()). Falls der Zielbereich
nicht überschrieben, sondern in ihn eingefügt werden soll, ist als Output-Iterator ein
Iterator zum Einfügen zu nehmen.
template <class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator first,
InputIterator last,
OutputIterator result);
template <class BidirectionalIterator1, class BidirectionalIterator2>
BidirectionalIterator2 copy_backward(BidirectionalIterator1 first,
BidirectionalIterator1 last,
BidirectionalIterator2 result);
303
Die Programmiersprache C++
copy_backward ist immer dann zu nehmen, wenn Ziel- und Quellbereich sich so
überlappen, daß der Anfang des Zielbereichs im Quellbereich liegt. result muß
anfangs auf das Ende des Zielbereichs zeigen.
swap_iter, swap und swap_ranges
swap() vertauscht zwei Elemente. Die beiden Elemente können in verschiedenen,
in demselben oder in keinem Container sein.
template <class T> void swap( T& a, T& b );
swap() ist spezialisiert für diejenigen Container, die eine Methode swap() zum
Vertauschen bereitstellen (deque, list, vector, set, map, multiset und
multimap).
iter_swap() nimmt zwei Iteratoren und vertauscht die dazugehörenden Elemente.
Die beiden Iteratoren können zu verschiedenen oder zu demselben Container
gehören
tempate <class ForwardIterator1, class ForwardIterator2>
void iter_swap(ForwardIterator1 a,
ForwardIterator2 b);
swap_ranges() vertauscht zwei Bereiche
transform
Kopieren mit gleichzeitigem Umwandeln.
template <class InputIterator, class OutputIterator, class UnaryOperation>
OutputIterator transform(InputIterator first,
InputIterator last,
OutputIterator result,
UnaryOperation op);
Auf jedes Element des Bereiches von first bis ausschließlich last wird die
Operation op angewendet und das Ergebnis in den mit result beginnenden
Bereich kopiert.
template <class InputIterator1, class InputIterator2
class OutputIterator,
class BinaryOperation>
OutputIterator transform(InputIterator1 first1,
InputIterator1 last1,
InputIterator2 first2,
OutputIterator result,
BinaryOperation bin_op);
replace
ersetzt in einer Sequenz jeden vorkommenden Wert old_value durch new_value.
template <class ForwardIterator, class T>
void replace(ForwardIterator first,
304
Die Programmiersprache C++
ForwardIterator last,
const T& old_value,
const T& new_value);
fill und fill_in
Falls eine Komponente ganz oder teilweise mit immer gleichen Werten, nämlich
Kopien von value vorbesetzt werden soll, eignen sich fill() oder fill_n().
template <class ForwardIterator, class T>
void fill(ForwardIterator first,
ForwardIterator last,
const T& value);
template <class OutputIterator, class Size, class T>
OutputIterator fill_n(OutputIterator first,
Size n,
const T& value);
generate und generate_n
Ein Generator in generate() ist ein Funktionsobjekt oder eine Funktion, die ohne
Parameter aufgerufen und deren Ergebnis den Elementen der Sequenz der Reihe
nach zugewiesen wird. Wie bei fill() gibt es eine Variante, die ein Iteratorpaar
erwartet, und eine Variante, die den Anfangsiterator und eine Stückzahl benötigt.
template <class ForwardIterator, class Generator>
void generate(ForwardIterator first,
ForwardIterator last,
Generator gen);
template <class OutputIterator, class Size, class Generator>
OutputIterator generate_n(OutputIterator first,
Size n,
generator gen);
remove und Varianten
Der Algorithmus entfernt alle Elemente aus einer Sequenz, die gleich einem Wert
value sind bzw. einem Prädikat pred genügen.
template <class ForwardIterator, class T>
ForwardIterator remove(ForwardIterator first,
ForwardIterator last,
const T& value)
template <class ForwardIterator, class Predicate>
ForwardIterator remove(ForwardIterator first,
ForwardIterator last,
Predicate pred);
unique
unique() löscht gleiche aufeinanderfolgende Elemente bis auf eins.
305
Die Programmiersprache C++
template <class ForwardIterator>
ForwardIterator unique(ForwardIterator first,
ForwardIterator last);
template <class ForwardIterator, class BinaryPredicate>
ForwardIterator unique(ForwardIterator first,
ForwardIterator last,
BinaryPredicate binary_pred);
template <class InputIterator, class OutputIterator>
OutputIterator unique(InputIterator first,
InputIterator last,
OutputIterator result);
template <class InputIterator, class OutputIterator, class BinaryPredicate>
OutputIterator unique_copy(InputIterator first,
InputIterator last,
OutputIterator result
BinaryPredicate binary_pred);
reverse
reverse() dreht die Reihenfolge der Elemente einer Sequenz um.
template <class BidirectionalIterator>
void reverse(BidirectionalIterator first,
BidirectionalIterator last);
template <class BidirectionalIterator, class OutputIterator>
OutputIterator reverse_copy(BidirectionalIterator first,
BidirectionalIterator last,
OutputIterator result);
rotate
rotate() verschiebt die Elemente einer Sequenz nach links, die vorne
herausfallenden Elemente werden hinten wieder eingefügt.
template <class ForwardIterator>
void rotate(ForwardIterator first,
ForwardIterator middle,
ForwardIterator last);
random_shuffle
random_shuffle() dient zum Mischen der Elemente einer Sequenz, also zur
zufälligen Änderung ihrer Reihenfolge. Die Sequenz muß Random-Access-Iteratoren
zur Verfügung stellen, z.B. vector oder deque
template <class RandomAccessIterator>
void random_shuffle(RandomAccessIterator first,
RandomAccessIterator last);
template <class RandomAccessIterator, class RandomNumberGenerator>
void random_shuffle(RandomAccessIterator first,
RandomAccessIterator last
RandomNumberGenerator& rand);
306
Die Programmiersprache C++
Die Mischung der Elemente soll gleichverteilt sein. Dies ist abhängig vom
verwendeten Zufallszahlengenerator. Die erste Variante benutzt eine interne
Zufallsfunktion.
Vom Zufallszahlengenerator oder der Zufallsfunktion wird erwartet, daß ein positives
Argument n vom Distanztyp des verwendeten Random-Access-Iterators genommen
und ein Wert zwischen 0 und (n-1) zurückgegeben wird.
partition
Eine Sequenz kann mit partition() so zerlegt werden, dass alle Elemente, die
einem bestimmten Kriterium pred genügen, anschließend vor allen anderen liegen.
Es wird ein Iterator zurückgegeben, der auf den Anfang des zweiten Bereichs zeigt.
Alle vor diesem Iterator liegenden Elemente genügen dem Prädikat.
template <class BidirectionalIterator, class Predicate>
BidirectionalIterator partition(BidirectionalIterator first,
BidirectionalIterator last,
Predicate pred);
sort
sort() sortiert zwischen den Iteratoren first und last. Der Algorithmus ist nur
für Container mit Random-Access-Iteratoren geeignet (z.B. deque, vector).
template <class RandomAccessIterator>
void sort(RandomAccessIterator first,
RandomAccesIterator last);
template <class RandomAccessIterator, class Compare>
void sort(RandomAccessIterator first,
RandomAccesIterator last,
Compare comp);
Der Aufwand ist im Mittel O ( N log N ) mit N = last – first.
Bsp.: Sortieren ganzer Zahlen172
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int anzN = 0;
int iwert;
vector<int> v;
cout << "Gib ganze Zahlen ein, ";
cout << "<Return nach jeder Zahl, <CTRL-Z am Ende:" << endl;
172
vgl. pr52801.CPP
307
Die Programmiersprache C++
while (cin >> iwert, cin.good())
{
v.push_back(iwert);
cout.width(6);
cout << anzN << ": " << v[anzN++] << endl;
}
if (anzN)
{
sort(v.begin(), v.end());
for (vector<int>:: const_iterator viter = v.begin();
viter != v.end(); viter++)
cout << *viter << " ";
cout << endl;
}
}
partial_sort
Teilweises Sortieren bringt die M kleinsten Elemente nach vorn. Der Rest bleibt
unsortiert. Der Algorithmus verlangt jedoch nicht die Zahl M, sondern einen Iterator
middle auf die entsprechende Position, so dass M = middle – first glilt.
template <class RandomAccessIterator>
void partial_sort(RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last);
template <class RandomAccessIterator>
void partial_sort(RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last
Compare comp);
nth-Element
Das n. größte oder das n. kleinste Element einer Sequenz mit Random-AccessIteratoren kann mit nth_element() gefunden werden.
Binäre Suche
Die C++-Standardbibliothek stellt vier Algorithmen zum Suchen und Einfügen in
sortierte Folgen bereit:
binary_search
template <class ForwardIterator, class T>
bool binary_search(ForwardIterator first,
ForwardIterator last,
const T& wert);
template <class ForwardIterator, class T, class Compare>
bool binary_search(ForwardIterator first,
ForwardIterator last,
const T& wert,
Compare comp);
lower_bound
308
Die Programmiersprache C++
template <class ForwardIterator, class T>
ForwardIterator lower_bound(ForwardIterator first,
ForwardIterator last,
const T& wert);
template <class ForwardIterator, class T, class Compare>
ForwardIterator lower_bound(ForwardIterator first,
ForwardIterator last,
const T& wert,
Compare comp);
upper_bound
Der Algorithmus findet die letzte Stelle, an der ein Wert wert eingefügt werden
kann, ohne die Sortierung zu zerstören. Der zurückgegebene Iterator zeigt auf diese
Stelle.
template <class ForwardIterator, class T>
ForwardIterator upper_bound(ForwardIterator first,
ForwardIterator last,
const T& wert);
template <class ForwardIterator, class T, class Compare>
ForwardIterator upper_bound(ForwardIterator first,
ForwardIterator last,
const T& wert,
Compare comp);
equal_range
Der Algorithmus ermiitelt den größtmöglichen Bereich, innerhalb dessen an jeder
beliebigen Stelle ein Wert value eingefügt werden kann, ohne die Sortierung zu
stören.
Mischen
Mischen, auch Verschmelzen genannt, ist ein Verfahren, zwei sortierte Folgen zu
einer einzigen zu vereinigen. Es werden schrittweise die jeweils ersten Elemente
beider Sequenzen verglichen, und es wird das kleinere (oder größere je nach
Sortierkriterium) Elemente in die Ausgabesequenz gepackt.
template <class InputIterator1, class InputIterator2, class OutputIterator>
OutputIterator merge(InputIterator first1,
InputIteratot last1,
InputIteratot first2,
InputIterator last2,
OutputIterator result);
template <class InputIterator1, class InputIterator2, class OutputIterator>
OutputIterator merge(InputIterator first1,
InputIteratot last1,
InputIteratot first2,
InputIterator last2,
OutputIterator result,
Compare comp);
merge() setzt eine vorhandene Ausgabeseqenz voraus. Falls eine der beiden
Eingabesequenzen erschöpft ist, wird der Rest der anderen in die Ausgabe kopiert.
309
Die Programmiersprache C++
Mengenoperationen auf sortierten Strukturen
Die folgenden Algorithmen beschreiben die grundlegenden Mengenoperationen wie
Vereinigung, Durchschnitt usw. auf sortierte Strukturen
includes
set_union
set_intersection
set_difference
set_symmetric_difference
Heap-Algorithmen
Wichtige Eigenschaften eines Heap
Die folgenden Heap-Eigenschaften bilden die Voraussetzung für die Anwendung der
Heap-Algorithmen:
- Die n Elemente eines Heap liegen in einem Array auf den Positionen 0 bis n – 1.
- Die Art der Anordnung der Elemente im Array entspricht einem vollständigen binären Baum, bei dem
alle Ebenen besetzt sind. Die einzige mögliche Ausnahme bildet die unterste Ebene, in der alle
Elemente auf der linken Seite erscheinen.
99
[0]
33
56
[1]
[2]
21
[3]
11
[7]
h[0]
99
30
20
48
[4]
[5]
[6]
9
25
1
10
17
40
[8]
[9]
[10]
[11]
[12]
[13]
[1]
33
[2]
56
[3]
21
[4]
30
[5]
20
[6]
48
[7]
11
[8]
9
[9]
25
[10]
1
[11]
10
[12]
17
[13]
40
Abb.: Array-Repräsentation eines Heap
Das Element h[0] ist die Wurzel, jedes Element h[j] , j > 0 hat einen Elternknoten h[(j-1)/2]
310
Die Programmiersprache C++
- Jedem Element h[j] ist eine Priorität zugeordnet, die größer oder gleich der Priorität der Kindknoten
h[2j+1] und h[j+2] ist173.
- Ein Array h mit n Elementen ist genau dann ein Heap, wenn h[(j-1)/2] >= h[j] für 1<=j<=n gilt. Daraus
folgt automatisch, dass h[0] das größte Element ist. Eine Priorityqueue entnimmt einfach das oberste
Element eines Heap. Anschließend wird er rekonstruiert, d.h. das nächstgrößte Element rückt an die
Spitze.
Die 4 Heap-Algorithmen
Sie sind auf alle Container, auf die mit Random-Access-Iteratoren zugegriffen
werden kann, anwendbar.
pop_heap()
Die Funktion pop_heap() entnimmt ein Element aus einem Heap.
template <class RandomAccessIterator>
void pop_heap(RandomAccessIterator first, RandomAccessIterator last);
template <class RandomAccessIterator, class Compare>
void pop_heap(RandomAccessIterator first, RandomAccessIterator last,
Compare comp);
Die Entnahme besteht darin, daß der Wert mit der höchsten Priorität der an der Stelle first steht, mit
dem Wert an der Stelle last – 1 vertauscht wird. Anschließend wird der Bereich (first, last – 1) in einen
Heap verwandelt. Die Komplexität von pop_heap() ist O (log( last  first )) .
push_heap()
Diese Funktion fügt ein Element einem vorhandenen Heap hinzu
template <class RandomAccessIterator>
void push_heap(RandomAccessIterator first, RandomAccessIterator last);
template <class RandomAccessIterator, class Compare>
void pop_heap(RandomAccessIterator first, RandomAccessIterator last,
Compare comp);
make_heap()
Diese Funktion sorgt dafür, daß die Heap-Bedingung für alle Elemente innerhalb eines Bereichs gilt.
template <class RandomAccessIterator>
void make_heap(RandomAccessIterator first,
RandomaccessIterator last);
template <class RandomAccessIterator, class Compare>
void make_heap(RandomAccessIterator first,
RandomaccessIterator last
Compare comp);
sort_heap()
Diese Funktion wandelt einen Heap in eine sortierte Sequenz. Die Sortierung ist nicht stabil, die
Komplexität ist O ( N log N ) , wenn N die Anzahl der zu sortierenden Elemente ist.
template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first,
RandomaccessIterator last);
173
Große Zahlen bedeuten hohe Prioritäten
311
Die Programmiersprache C++
template <class RandomAccessIterator, class Compare>
void sort_heap(RandomAccessIterator first,
RandomaccessIterator last
Compare comp);
Die Sequenz ist aufsteigend sortiert.
Minimum und Maximum
Die inline-Templates min() und max() geben jeweils das kleinere (bzw. das
größere) von zwei Elementen zurück. Bei Gleichheit wird das erste Element
zurückgegeben.
template <class T>
const T& min(const T& a, const T& b);
template <class T, class Compare>
const T& min(const T& a, const T& b, Compare comp);
template <class T>
const T& max(const T& a, const T& b);
template <class T, class Compare>
const T& max(const T& a, const T& b, Compare comp);
Lexikographischer Vergleich
Er dient zum Vergleich zweier Sequenzen, die auch verschiedene Längen haben
können.
Permutationen
Eine Permutation entsteht aus einer Sequenz durch Vertauschen zweier Elemente.
So ist (0,2,1) bspw. eine Permutation, die aus (0,1,2) entstanden ist. Für eine
Sequenz mit N Elementen gibt es N! Permutationen. Man kann sich die Menge aller
N! Permutationen einer Sequenz geordnet vorstellen. Die Ordnung kann man mit
dem <-Operator oder einem Vergleichsobjekt comp herstellen.
template <class BidirectionalIterator>
bool prev_permutation(BidirectionalIterator first,
BidirectionalIterator last);
template <class BidirectionalIterator, class Compare>
bool prev_permutation(BidirectionalIterator first,
BidirectionalIterator last,
Compare comp);
template <class BidirectionalIterator>
bool next_permutation(BidirectionalIterator first,
BidirectionalIterator last);
template <class BidirectionalIterator, class Compare>
bool next_permutation(BidirectionalIterator first,
BidirectionalIterator last,
Compare comp);
312
Die Programmiersprache C++
Wenn eine Permutation gefunden wird, ist der Rückgabewert true. Andernfalls
handelt es sich um das Ende eines Zyklus. Dann wird false zurückgegeben und die
Sequenz in die kleinstmögliche (bei next_permutation()) bzw. die größtmögliche
(bei prev_permutation()) entsprechend dem Sortierkriterium verwandelt.
313
Die Programmiersprache C++
5.5 Nationale Besonderheiten
Die Klasse locale (Header <locale>) bestimmt die nationalen Besonderheiten
von Zeichensätzen.
5.6 Die numerische Bibliothek
5.6.1 Komplexe Zahlen
Komplexe Zahlen werden im Header <complex> durch spezialisierte Templates für
float, double und long double realisiert.
5.6.2 Grenzwerte von Zahlentypen
5.6.3 Numerische Algorithmen
accumulate
Der Algorithmus addiert auf einen Startwert alle Werte *i eines Iterators i von
first bis last.
template <class InputIterator, class T>
T accumulate(InputIterator first,
InputIterator last,
T init);
template <class InputIterator, class T, class binaryOperation>
T accumulate(InputIterator first,
InputIterator last,
T init,
binaryOperation binOp);
inner_product
Der Algorithmus addiert das Skalarprodukt zweier Container u und v, die meistens
Vektoren sein werden, auf den Anfangswert init: Ergebnis = init +  u i  vi
i
template <class InputIterator, class InputIterator , class T>
T inner_product(InputIterator1 first1,
InputIterator1 last1,
InputIterator2 first2,
T init);
template <class InputIterator, class InputIterator , class T,
314
Die Programmiersprache C++
class binaryOperation1, class binaryOperation2>
T inner_product(InputIterator1 first1,
InputIterator1 last1,
InputIterator2 first2,
T init,
binaryOperation1 binOp1,
binaryOperation2 binOp2);
partial_sum
template <class InputIterator, class OutputOperator>
OutputIterator partial_sum(InputIterator first,
InputIterator last,
OutputOperator result);
template <class InputIterator, class OutputOperator, class binaryOperation>
OutputIterator partial_sum(InputIterator first,
InputIterator last,
OutputOperator result
BinaryOperation binOp);
adjacent_difference
Dieser Algorithmus berechnet die Differenz zweier aufeinanderfolgender Elemente
eines Containers v und schreibt das Ergebnis in einen Ergebniscontainer e, auf den
vom Iterator result verwiesen wird. Da es genau einen Differezwert weniger als
Elemente gibt, bleibt das erste Element erhalten. Falls das erste Element den Index
0 trägt, gilt:
e0 = v0
ei = vi – vi-1
Außer Differenzbildung sind andere Operatoren möglich.
template <class InputIterator, class OutputOperator>
OutputIterator adjacent_difference(InputIterator first,
InputIterator last,
OutputOperator result);
template <class InputIterator, class OutputOperator, class binaryOperation>
OutputIterator adjacent_difference(InputIterator first,
InputIterator last,
OutputOperator result
BinaryOperation binOp);
315
Die Programmiersprache C++
5.6.4 Optimierte numerische Arrays (valarray)
Der Header <valarray> schließt die Template-Klasse valarray ein, die für
mathematische Vektor-Operationen gedacht ist. Ein valarray-Objekt ist ein für
numerische Berechnungen optimierte Vekor.
5.6.4.1 Konstruktoren und Elementfunktionen
5.6.4.2 Binäre Valarray-Operatoren
5.6.4.3 Mathematische Funktionen
5.6.4.4 slice
5.6.4.5 slice_array
5.6.4.6 gslice
5.6.4.7 gslice_array
5.6.4.8 mask_array
5.6.2.9 indirect_array
316
Die Programmiersprache C++
5.7 Typerkennung zur Laufzeit
5.8 Speichermanagement
5.8.1 <new>
Der Header <new> enthält die Operatoren new, new[] und delete[].
5.8.2 <memory>
317
Die Programmiersprache C++
6. Windows-Programmierung unter Visual C++ mit MFC
6.1 Merkmale von Visual C++ .NET und MFC
6.1.1 Visual C++ -Features und MFC -Grundlagen
Compiler
Der Microsoft Visual C++ -Compiler stellt eine 32-Bit-Foundation-Classes-Bibliothek
zur Verfügung, die einen Satz objektorientierter Programmierwerkzeuge für die
Entwicklung von 32-Bit-Anwendungen enthält
Ressourcen-Editor
Ressourcen bestimmen die Benutzerschnittstellen von Windows-Programmen.
Ressourcen eines Programms sind bestimmte Elemente der Benutzeroberfläche:
Dialoge
Menüs
Tastaturkürzel
Bitmaps
Anwendungsbereiche
Zeigersymbole
Stringtabellen
HTML-Code
Versionsinformationen
Benutzerdefinierte Ressourcen
Ressourcen werden nicht durch C++-Anweisungen aufgebaut. Statt dessen bedient
man sich zum Erstellen von Ressourcen spezieller Editoren, speichert die
Ressourcen in speziellen Dateien (mit der Extension .rc, Ressourcenskriptdateien)
und verwendet im C++-Quelltext spezielle Methodenaufrufe zum Laden der fertigen
Ressourcen.
Integrierte Entwicklungsumgebung (IDE)
Visual C++ .NET ist eingebettet in eine integrierte Entwicklungsumgebung, die
verschiedene Entwicklungswerkzeuge in einer einzigen, leicht zu bedienenden
Umgebung zusammenfaßt. Die Visual C++ -Umgebung enthält folgende
Hauptkomponenten:
Editoren
Compiler
Debugger
Projekt-Manager
Browser
Hilfe zum Verständnis der Beziehungen verschiedener Objekte in objektorientierten Programmen.
Programmassistenten
Bei diesen Assistenten handelt es sich um Utilities, die als einzige Schnittstelle zum Anwender eines
oder mehrere Dialogfelder anzeigen, über die auf die Arbeit des Assistenten eingewirkt werden kann.
Nachdem der Anwender seine Angaben abgeschlossen hat, erstellt der Assistent ein passendes
Projekt, legt erste Dateien an und setzt ein Codegerüst auf, d.h. der Anwender wird von den mehr
formalen Programmierangaben (z.B. Doc/View-Gerüst für MFC-Angaben, Einrichten einer
Datenbankanbindung, Registrierung für ActiveX-Steuerelementanbindung) befreit.
Eigenschaftsfenster
Sie steuern das Verhalten von Visual C++.
318
Die Programmiersprache C++
Die Windows-API
Da Windows eine geschützte Entwicklung von Microsoft ist und der Sourcecode
nicht offengelegt ist, benötigen die Anwender einen Zugriff auf Windows-interne
Funktionen – ein Application Programming Interface.
Das Windows-API fasst eine große Anzahl Windows-Funktionen in einer
Schnittstelle zusammen. Ältere Windows-API-Funktionen basieren auf reiner CProgrammierung und boten nur reine C-Funktionen an. Später wurde eine
objektorientierte Schicht darüber gesetzt – die Microsoft Foundation Classes
(MFC).
Anwendungsgerüst
Anwendungsgerüste vereinfachen die GUI-Programmierung. Sie stellen einen Satz
von C++-Klassen bereit, die nachbilden, wie GUI-Programme arbeiten. Das
Application-Framework, das zu Visual C++ gehört, ist eine Klassenbibliothek mit dem
Namen: Microft Foundation Classes (MFC). Visual C++ schließt noch eine andere
Art von Anwendungsgerüst mit ein: Die ActiveX Template Library (ATL). Das ist
eine Sammliung von C++-Libraries, die es einfach machen, Objekte für das
Component Object Model (COM) zu erzeugen.
MFC-Bibliothek
Bibliotheken wie die MFC-Bibliothek beginnen mit wenigen Basisklassen. Von diesen
Basisklassen werden weitere Klassen abgeleitet. CObject ist eine Basisklasse, die
extensiv in der Entwicklung von Windows-Umgebungen verwendet wird. HeaderDateien der MFC-Bibliothek befinden sich im Unterverzeichnis atlmfc/include.
6.1.2 Funktionsweise von Windows-Programmen
Komponenten prozedurorientierter Windows-Anwendungen
Alle Windows-Anwendungen enthalten zwei Komponenten
-
die Funktion WinMain() dient als Einstiegspunkt einer Windows-Anwendung und verhält sich
ähnlich wie die main()-Funktion in gewöhnlichen C++-Programmen
die Fensterfunktion. Eine Windows-Anwendung greift nie direkt auf eine Fensterfunktion zu. Falls
eine Windows-Anwendung eine Standard-Fensterfunktion ausführen will, muß sie Windows
auffordern, die angegebene Aufgabe auszuführen. Windows-Anwendungen besitzen deshalb eine
sog. Callback-Fensterfunktion. Sie wird in Windows registriert und aufgerufen, wenn Windows eine
Fensteroperation ausführt.
Anlegen eines Win-API-Projekts
1. Aufruf des Befehls DATEI/NEU, Wechseln zur Seite PROJEKTE des Dialogfelds NEU; Angabe
des Namens und des Verzeichnisses vom Projekt, Anlegen eines neuen Arbeitsbereichs, Wahl
des Projekttyps WIN32-ANWENDUNG.
2. Entscheidung im nachfolgenden Dialogfeld für EINE EINFACHE WIN32-ANWENDUNG zur
Einrichtung einer Quelltextdatei für das Programm.
3. Aufruf der Quelltextdatei zur Betrachtung des Aufrufs der WinMain()-Funktion.
#include "stdafx.h"
319
Die Programmiersprache C++
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR
lpCmdLine,
int
nCmdShow )
{
// ZU ERLEDIGEN: Fügen Sie hier den Code ein.
return 0;
}
Die Eintrittsfunktion WinMain()
WinMain() wird von Windows aufgerufen und über Parameter mit passenden
Argumenten versehen:
hInstance. Windows muß alle im System existierenden Objekte (Anwendungen, Fenster,
Gerätekontexte, Dateien, etc.) verwalten. Dazu muß es diese Objekte natürlich auch identifizieren und
ansprechen können. Zu diesem Zweck weist Windows den Objekten sog. Handles zu – so auch jeder
neu aufgerufenen Anwendung. Windows übergibt der Anwendung ihr eigenen Handle als Argument an
hInstance. Diese Zahl identifiziert das Programm, wenn es unter Windows läuft.
hPrevInstance. Relikt aus Win16, unter Win32 wird stets NULL übergeben.
lpCmdLine. Hier erfogt die Übergabe der Kommandozeile des Programms
nCmdShow. Dieser Parameter legt das Erscheinungsbild des Anwendungsfensters auf dem Desktop
fest, z.B. ob das Fenster in normaler Größe, als Vollbild oder als Symbol aufgerufen werden soll174.
Erzeugen des Hauptfensters
HWND CreateWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HANDLE hInstance,
LPVOID lpParam
);
//
//
//
//
//
//
//
//
//
//
//
Zeiger auf registr. Fensterklasse
Zeiger auf Fenstername
Fensterstil
Horizonatale Position
Vertikale Position
Breite des Fensters
Hoehe des Fensters
Übergeordnetes Fenster / Besitzer
Handle des Menüs
Handle der Anwendung
Zeiger auf Fensterdaten
Fensterklassen. Unter Windows ist eine Fensterklasse eine Struktur, denn Windows
ist nicht objektorientiert.
typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;
174
// Fensterfunktion
vgl. CWnd::ShowWindow() in OnLine-Hilfe
320
Die Programmiersprache C++
Eine Anwendung kann ihre eigene Fensterklasse definieren, indem sie eine Struktur
des entsprechenden Typs erstellt und danach die Felder mit Informationen über die
Fensterklasse füllt.. Nach Anmelden der Fensterklasse unter Windows können auf
der Basis dieser Fensterklasse Fenster erzeugt werden. Alle Fenster einer
Fensterklasse teilen sich die Elemente, die in der Fensterklasse definiert sind.
Einige Fensterklassen sind in Windows vordefiniert und registriert, z.B. die Klassen
für die Standardsteuerelemente. So heißt die Klasse für Schaltflächen BUTTON. Auf
ihrer Grundlage kann man eigene Fenster erstellen, z.B.:
hwnd = CreateWindow("BUTTON","Hallo Welt",
WS_VISIBLE | BS_CENTER, 100, 100, 100, 80,
NULL, NULL, hInstance, NULL);
Definition einer Fensterklasse. Der folgende Quelltextauszug definiert eine
Fensterklasse für Hauptfenster:
WNDCLASS WinClass;
// Fensterklasse
// Speicher reservieren und Variable einrichten
memset(&WinClass, 0, sizeof(WNDCLASS));
WinClass.style = CS_HREDRAW | CS_VREDRAW
WinClass.lpfnWndProc = WndProc;
WinClass.hInstance = hInstance;
WinClass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
WinClass.hCursor = LoadCursor(NULL,IDC_ARROW);
WinClass.lpszClassName = "Windows-Programm";
Bis auf WndProc sind alle Werte vordefiniert. WndProc muß mit passenden
Parametern implementiert werden:
// Fensterfunktion WinProc
LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage,
WPARAM wParam, LPARAM lParam)
{
return 0; // Rückgabewert 0, da die Funktionsweise noch nicht bekannt
}
Registrierung der Fensterklasse. Zur Registrierung der Fensterklasse verwendet
man die API-Funktion RegisterClass():
if (!RegisterClass(&WinClass)) return(FALSE);
Erzeugen der Fensterklasse. Nachdem die Fensterklasse registriert ist, kann auf
Grundlage der Fensterklasse ein Fenster erzeugt und angezeigt werden.
HWND hWindow;
// Fenster-Handle
// Erstelle Hauptfenster der Anwendung
hWindow = CreateWindow("Windows-Programm",
"API-Programm",
WS_OVERLAPPEDWINDOW, 10, 10,
400, 300, NULL, NULL, hInstance, NULL);
ShowWindow(hWindow, nCmdShow);
UpdateWindow(hWindow);
Die Funktion ShowWindow() ist erforderlich, um ein Fenster tatsächlich anzuzeigen. Der Parameter
hWindow ist das Handle des Fensters, das durch Aufruf von CreateWindow() erstellt wird. Der
zweite Parameter nCmdShow bestimmt, wie das Fenster anfänglich angezeigt wird (Sichbarkeitsstatus
des Fensters). Falls bspw. nCmdShow durch die Konstante SW_SHOWINACTIVE ersetzt wird, die in
winuser.h definiert ist, wird das Fenster als Symbol angezeigt. Andere Anzeigemöglichkeiten sind
321
Die Programmiersprache C++
SW_SHOWMAXIMIZED, die das Fenster aktiviert und bildschirmfüllend anzeigt, und das Gegenstüch
SW_SHOWMINIMIZED.
Der letzte Schritt zur Anzeige des Fensters ist UpdateWindow(hWindow). Der Aufruf von
UpdateWindow() generiertdie WM_PAINT-Meldung, die dafür sorgt, dass der Arbeitsbereich
aufgebaut wird.
Vordefinierte Fensterklassen. Auf der graphischen Benutzeroberfläche können
beliebig konfigurierte Fenster platziert werden. Jedes Dialogfeld, jedes Eingabefeld,
jede Schaltfläche und jedes Textfeld ist ein Fenster. Die Windows-API besitzt einige
Fensterklassen mit bereits vordefinierten Verhalten, die wichtigsten davon sind:
Eingabefelder (edit), Schaltflächen (button), Textfelder, Komboboxen (combobox),
Listenfelder (listbox).
Erzeugen des Hauptfensters für die Anwendung.
1. Übertrage den vorstehend aufgeführten Code zur Erzeugung eines Fensters in
das vorliegende API-Programm
// pr00010.cpp : Definiert den Einsprungpunkt für die Anwendung.
//
#include "stdafx.h"
// Vorwaertsdeklaration
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR
lpCmdLine,
int
nCmdShow )
{
// ZU ERLEDIGEN: Fügen Sie hier den Code ein.
HWND hWindow; // Fenster-Handle
WNDCLASS
WinClass; // Fensterklasse
// Speicher reservieren und Variable einrichten
memset(&WinClass, 0, sizeof(WNDCLASS));
WinClass.style = CS_HREDRAW | CS_VREDRAW;
WinClass.lpfnWndProc = WndProc;
WinClass.hInstance = hInstance;
WinClass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
WinClass.lpszClassName = "Windows-Programm";
// Fensterklasse anmelden
if (!RegisterClass(&WinClass))
return(FALSE);
// Erstelle Hauptfenster der Anwendung
hWindow = CreateWindow("Windows-Programm",
"API-Programm",
WS_OVERLAPPEDWINDOW, 10, 10,
400, 300, NULL, NULL, hInstance, NULL);
ShowWindow(hWindow, nCmdShow);
UpdateWindow(hWindow);
return 0;
}
// Fensterfunktion WinProc
LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage,
WPARAM wParam, LPARAM lParam)
{
return 0;
}
322
Die Programmiersprache C++
2. Ausführen des Programms (Strg + F5)
Das Hauptfenster des Programms flackert kurz auf und verschwindet gleich wieder,
weil die Anwendung sofort nach dem Aufruf beendet wird. Es fehlt eine
Warteschleife, in der die Anwendung auf Windows-Nachrichten wartet und diese
verarbeitet.
6.1.3 Eintritt in die Nachrichtenverarbeitung
Windows fängt alle Benutzerereignisse (Eingaben über Maus und Tastatur) ab und
schickt sie in Form von Nachrichten an die betreffenden Anwendungen, genauer
gesagt an eine spezielle Warteschlangenstruktur (Message-Queue), die Windows für
jede laufende Anwendung einrichtet.
Die Message-Queue kann Meldungen enthalten, die von Windows generiert wurden,
oder Meldungen, die von anderen Anwendungen gesendet wurden.
Die Message Loop.
Zum Auslesen der eingetroffenen Nachrichten muß die Anwendung eine Schleife
implementieren, in der sie ständig die Nachrichtenwarteschlange nach Nachrichten
abfragt und diese gegebenenfalls ausliest.
// main message loop
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranlateAccelerator(msg.hwnd, hAccelTable, &msg))
TransLateMessage(&msg);
DispatchMessage(&msg);
}
GetMessage() kopiert die Meldung in die Meldungsstruktur, auf die der long-Zeiger &msg verweist
und übergibt die Meldungsstruktur an die Hauptfunktion des Programms. Der NULL-Parameter weist
die Funktion an, jede Meldung für jedes Fenster abzufragen, das zu der Anwendung gehört. Die letzten
beiden Parameter weisen GetMessage() an, keine Meldungsfilter anzuwenden. Meldungsfilter
können die abgefragten Meldungen auf spezielle Kategorien, z.B. bestimmte Tastatureingaben oder
Mausbewegungen, beschränken175. Die Meldung WM_QUIT ist die einzige Meldung, mit der eine
Anwendung die Meldungsschleife beeinflussen kann.
Die Funktion TranslateMessage() wandelt VirtualKey-Meldungen in Zeichen-Meldungen. Sie wird
nur bei Anwendungen benötigt, die Zeichen verarbeiten müssen, die über die Tastatur eingegeben
werden.
Die Funktion DispatchMessage() sendet Widows aktuelle Meldungen an die korrekten
Fensterprozeduren. Mit dieser Funktion können leicht zusätzliche Fenster und Dialogfelder zu einer
Anwendung hinzugefügt werden. DispatchMessage() leitet jede Meldung an die entsprechende
Fensterprozedur weiter.
Nachrichten, die die Anwendung über vom Anwender ausgelöste Ereignisse
informieren, laufen stets über die MessageLoop:
1. Ereignis tritt auf, bspw. die Bewegung der Maus
175
Die Filter heißen wMsgFilterMin und wMsgFilterMax. Sie geben an, welche Grtenzwerte für die
numerischen Filter gelten sollen.
323
Die Programmiersprache C++
2. Ereignis wird in die Message Queue eingetragen. Die System Message Queue –Warteschleife
fängt erst einmal alle Eingaben ab, die von Perepheriegeräten kommen (Maus, Tastatur, Drucker,
etc.)
3. Die System Message Queue wird abgearbeitet (First in, First out). Die abgefangenen Ereignisse
werden ausgelesen und auf die Message Queue der zugehörigen Threads verteilt. Klickt bspw. der
Anwender in ein Fenster, wird der Thread ermittelt, der dieses Fenster erzeugt hat, und die
Botschaft wird in seine Message Queue eingetragen.
4. Die Botschaft wird in der MessageLoop empfangen, übersetzt (in für den Programmierer leichter
zu lesende Parameter) und an die bearbeitende Fensterfunktion weitergereicht.
5. Die Fensterfunktion des Fensters erhält die Botschaft. Letztendlich werden Nachrichten an Fenster
geschickt. Daher definiert jedes Fenster eine eigene Fensterfunktion, die einkommende
Botschaften empfängt und einer passenden Bearbeitung zuführt.
Windows
Mausbewegung
Anwendung
Eintrag in SystemQueue
Anwendung wird in CPU geladen
WM_MOUSEMOVE
MessageLoop fordert Meldung an
fordert
Botschaft
Eintrag in
Application-Queue
WM_MOUSEMOVE
MessageLoop verarbeitet
Meldung
WM_MOUSEMOVE
Windows wird angewiesen,
Fensterfunktion aufzurufen
Fensterfunktion
(reagiert auf Mausbewegung)
Abb.: Ereignisbehandlung von Windows
Nachrichten, die die Anwendung über vom Anwender ausgelöste Ereignisse
informieren, laufen stets über die Message Loop.
Die Fensterfunktion
Windows ruft die Fensterfunktion des Fensters auf, an das die Nachricht gerichtet
ist, und die Nachricht als Argument an die Parameter der Funktion übergibt.
Aufgabe des Programmierers ist es, in der Funktion abzufragen, welche Nachricht
empfangen wurde und dann auf die jeweilige Nachricht passend zu reagieren. Zu
diesem Zweck richtet man in der Fensterfunktion eine switch-Verzweigung ein und
für jede zu behandelnde Funktion einen case-Block. Da der Programmierer nicht für
alle der über 200 Nachrichten Code zur Beantwortung bereitstellen kann, ist in
Windows die Funktion DefWindowProc() definiert, der man im default-Block alle
Nachrichten, die man nicht selbst behandeln möchte, weiterreicht. Eine Nachricht
muß man aber auf jedem Fall behandeln: WM_DESTROY. Damit die Anwendung
beendet wird, muß die Message Loop der Anwendung beendet werden. Das
geschieht, in dem man als Antwort auf die WM_DESTROY-Nachricht die API-Funktion
PostQuitMessage() aufruft. Diese schickt ihrerseits eine WM_QUIT-Nachricht an
die Anwendung, diese beendet die Message Loop.
324
Die Programmiersprache C++
Windows verfügt über mehrere hundert verschiedene Windows-Meldungen, die es
an die Fensterfunktion senden kann. Die Meldungen haben Bezeichner, die mit WM_
beginnen. Bspw. werden WM_COMMAND, WM_DESTROY, WM_INITDIALOG und
WM_PAINT recht häufig verwendet. Die Bezeichner werden auch als symbolische
Konstanten bezeichnet. Im nachfolgenden Beispiel wird eine Meldung mit der
symbolischen Konstanten WM_LBUTTONDOWN (Drücken der linken Maustaste)
behandelt. Alle Mausbewegungen werden als Nachrichten kodiert:
WM_MOUSEMOVE
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_RBUTTONDOWN
WM_RBUTTONUP
WM_MBUUTONDOWN
WM_MBUTTONUP
Maus wird bewegt
Linke Maustaste wird gedrückt
Linke Maustaste wird losgelassen
Rechte Maustaste wird gedrückt
Rechte Maustaste wird losgelassen
Mittlere Maustaste wird gedrückt
Mittlere Maustaste wird losgelassen
Die Nachrichtenparameter enthalten die Koordinaten und weitere Informationen:
signed int xPos = LOWORD(lParam); // X-Koordinate
signed int yPos = HIWORD(lParam); // Y-Koordinate
Die Koordinaten relativ zum Fensterursprung (linke obere Ecke).
Bsp. : Eigene Nachrichtenbehandlung nach
WM_LBUTTONDOWN).
Drücken der linken Maustaste (
// Fensterfunktion WndProc
LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage,
WPARAM wParam, LPARAM lParam)
{
char str[30] = "Hier erfolgte ein Mausklick";
HDC dc;
// Beantworte Nachrichten mit entspechenden Aktionen
switch(uiMessage)
{
case WM_LBUTTONDOWN:
dc = GetDC(hWnd);
TextOut(dc,LOWORD(lParam),
HIWORD(lParam),
str, strlen(str));
// Gerätekontext freigeben
ReleaseDC(hWnd, dc);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, uiMessage, wParam, lParam);
// Alle Nachrichten, die nicht mit eigenen Antwort-Funktionen
// verbunden werden.
}
}
Der erste Parameter von WndProc ist hWnd. Das Handle hWnd ist ein Zeiger auf das Fenster, an das
Windows die Meldung sendet. Da eine Fensterfunktion Meldungen für mehrere Fenster verarbeiten
kann, die mit derselben Fensterklasse erstellt werden, verwendet die Fensterfunktion dieses Handle,
um festzustellen, welches Fenster die Meldung empfangen soll.
Der zweite Parameter der Funktion, uiMessage, gibt die tatsächliche Meldung an, die verarbeitet
werden soll. Die Meldung ist in winuser.h definiert.
325
Die Programmiersprache C++
Die beiden letzten Parameter, wParam und lParam, enthalten Informationen zur Verarbeitung der
jeweiligen Meldung. Bei vielen Funktionen haben diese Parameter den Wert Null.
Verarbeitung von WM_PAINT-Meldungen. WM_PAINT verarbeitet alle WindowsPAINT-Meldungen. Hier kann die Windows-Funktion BeginPaint() aufgerufen
werden. Diese Funktion bereitet das angegebene Fenster auf die Darstellung
("Painting") vor und füllt eine PAINTSTRUCT(&ps) mit Daten über den Bereich, der
dargestellt werden soll. Die Funktion BeginPaint() gibt ein Handle auf den
Gerätekontext des gegebenen Femsters zurück.
Nachrichtenübertragung ohne MessageLoop
Eine Reihe von Nachrichten haben nur indirekt etwas mit Benutzeraktionen zu tun
und werden intern von Windows verschickt (z.B. Windows informiert ein Fenster
darüber, daß es gerade verschoben, neu dimensioniert oder geschlossen wird).
Windows umgeht dabei auch die Message Queues der einzelnen Threads und
schickt die Nachricht statt dessen direkt an die Fensterfunktion des betroffenen
Fensters.
1. Ereignis tritt auf (z.B. der Anwender hat in der Titelleiste eines Fensters die Schaltfläche zum
Schließen geglickt).
2. Windows schickt eine entsprechende Botschaft direkt an die Fensterfunktion(en) des oder der
betroffenen Fenster (in diesem Bsp. WM_CLOSE).
3. Die Fensterfunktion empfängt die Botschaft und führt sie einer korrekten Verarbeitung zu.
4. Ereignis tritt auf (z.B. der Anwender hat in der Titelleiste eines Fensters die Schaltfläche zum
Schließen geglickt).
5. Windows schickt eine entsprechende Botschaft direkt an die Fensterfunktion(en) des oder der
betroffenen Fenster (in diesem Bsp. WM_CLOSE).
6. Die Fensterfunktion empfängt die Botschaft und führt sie einer korrekten Verarbeitung zu.
326
Die Programmiersprache C++
6.1.4 Vom API zur MFC
Konzepte der API-Programmierung sind in der MFC gekapselt.
WinMain() und Anwendungsobjekt
MFC-Programme definieren keine WinMain()-Eintrittsfunktion. Statt dessen wird
eine globale Instanz der Anwendungsklasse definiert. Die WinMain()Eintrittsfunktion ist im Code der MFC versteckt (Modul AppModul.cpp) und wird bei
Verwendung der MFC-Bibliotheken automatisch mit eingebunden. Die WinMain()Funktion ruft die globale MFC-Funktion AfxWinMain() auf. Diese greift auf das
Anwendungsobjekt des Programms zu und ruft dessen InitInstance()-Methode
auf, in der alle wichtigen Initialisierungsarbeiten von der Auswertung bis zur
Erzeugung des Hauptfensters vorgenommen werden.
Message Loop und Run()
Die Message Loop der API-Programme ist in der Run()-Methode der MFC-Klasse
CWinApp gekapselt. Aufgerufen wird die Methode in der MFC-Funktion
AfxWinMain().
Erzeugung der Fenster
In API-Programmen erstellt man Fenster durch Einrichtung und Registrierung einer
Fensterklasse und der nachfolgenden Erzeugung eines Fensters von dieser
Fensterklasse.
In der MFC erzeugt man zuerst ein Objekt einer MFC-Fensterklasse und ruft dann
die Methode Create() auf, die das eigentliche Fenster erzeugt und mit dem Objekt
verbindet. Der Methode Create() kann man wie der API-Funktion
CreateWindow() eine eigene Fensterklasse übergeben.. Übergibt man in der
Create()-Methode als erstes Argument den Wert NULL, wird die
Standardfensterklasse von CWnd verwendet.
In vielen Fällen braucht man für das Hauptfenster der Anwendung die Methode
Create() nicht selbst aufzurufen. Man kann die Methode LoadFrame()
verwenden oder – im Fall von Doc/View-Anwendungen – die ganze Arbeit vom
Konstruktor der Dokumentenklasse erledigen lassen.
Zur Beibehaltung der Kontrolle über die Erzeugung und Konfiguration der Fenster,
kann man die Methoden PreCreateWindow() und OnCreate() überschreiben.
Der Parameter nCmdShow der WinMain()-Funktion entspricht der MFC-Variablen
m_nCmdShow, die in der Methode InitInstance() an die Methode
ShowWindow() übergeben werden kann.
Fensterfunktion und Antworttabellen
Nachrichten werden in API-Programmen in Fensterfunktionen behandelt.
Fensterfunktionen enthalten eine switch-Anweisung, in der für alle zu
327
Die Programmiersprache C++
behandelnden Nachrichten case-Blöcke vorgesehen werden. In dem case-Block
steht dann der Code, der als Antwort auf die Nachricht ausgeführt wird. Ist dieser
Code umfangreicher, dann kann man ihn auch in eine eigene Funktion auslagern,
die dann im case-Block aufgerufen wird. Würde man dies für alle behandelten
Nachrichten machen, wäre die Fensterfunktion genauso aufgebaut wie eine
Antworttabelle.
In MFC-Programmen werden Nachrichten in Antworttabellen (MESSAGE_MAP)
weitergeleitet. In der Antworttabelle (Meldungs-) ist festgehalten, für welche
Nachricht welche Methode zur Beantwortung aufgerufen werden soll.
Nachrichtentabellen sind ein fester Bestandteil der Windows-Programmierung mit
MFC. Nachrichtentabellen werden in der Regel von den Assistenten erzeugt und
bestehen aus zwei Teilen. Der erste Teil befindet sich in der Headerdatei und der
zweite Teil in der entsprechenden .cpp Datei. Die Nachrichtentabellen sind in Form
von Makros realisiert worden. Die Deklaration einer Nachrichtentabelle in der
Headerdatei einer Anwendung, die vom Anwendungs-Assistenten erzeugt wurde,
kann bspw. folgendermaßen aussehen:
//{{AFX_MSG(CPr00011App)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
Der zweite Teil der Nachrichtentabelle in der entsprechenden .cpp Datei sieht so
aus:
BEGIN_MESSAGE_MAP(CPr00011Dlg, CDialog)
//{{AFX_MSG_MAP(CPr00011Dlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_WM_LBUTTONDOWN()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
Die ON_-Makros der Antworttabelle erfüllen die gleiche Aufgabe wie die case-Blöcke
der Fensterfunktion, z.B.:
case WM_LBUTTONDOWN:
ON_WM_LBUTTONDOWN()
Makros der Meldungstabelle
In Meldungstabellen werden eine Reihe von Makros verwendet, z.B.:
DECLARE_MESSAGE_MAP
BEGIN_MESSAGE_MAP
END_MESSAGE_MAP
ON_COMMAND
ON_COMMAND_RANGE
ON_CONTROL
ON_CONTROL_RANGE
Wird in der Headerdatei verwendet, um anzuzeigen, daß es
eine Meldungstabelle in der Quelldatei gibt
Markiert den Anfang der Meldungstabelle in der Quelldatei
Markiert das Ende einer Meldungstabelle in der Quelldatei
Delegiert die Behandlung eines speziellen Befehls an eine
Memberfunktion der Klasse
Delegiert die Behandlung einer speziellen Gruppe von
Befehlen, repräsentiert durch eine Bereich von Befehls-IDs,
an eine einzelne Memberfunktion der Klasse
Delegiert die Behandlung einer speziellen Benachrichtigung
für ein benutzerdefiniertes Steuerelement an eine
Memberfunktion der Klasse
Delegiert die Behandlung einer Benachrichtigung einer
328
Die Programmiersprache C++
Gruppe
von
benutzerdefinierten
Steuerelementen,
repräsentiert durch einen Bereich von Steuerelement-IDs, an
eine Memberfunktion der Klasse
ON_MESSAGE
Delegiert die Behandlung einer benutzerdefinierten Meldung
an eine Memberfunktion der Klasse
ON_REGISTERED_MESSAGE
Delegiert
die
Behandlung
einer
registrierten
benutzerdefinierten Meldung an eine Memberfunktion der
Klasse.
ON_UPDATE_COMMAND_UI
Delegiert die Aktualisierung eines speziellen Befehls an eine
Memberfunktion der Klasse
ON_COMMAND_UPDATE_UI_RANGE Delegiert die Aktualisierung einer Gruppe von Befehlen,
repräsentiert durch einen Bereich von Befehls-IDs, an eine
einzelne Memberfunktion der Klasse.
ON_NOTIFY
Delegiert
die
Behandlung
einer
Steuerelementbenachrichtigung mit Zusatzdaten an eine Memberfunktion
der Klasse.
ON_NOTIFY_RANGE
Delegiert
die
Behandlung
einer
Gruppe
von
Steuerelementbenachrichtigungen
mit
Zusatzdaten,
repräsentiert durch einen Bereich von Kindbezeichnern, an
eine
einzelne
Memberfunktion
der
Klasse.
Die
Steuerelemente, die diese Benachrichtigung senden, sind
Kindfenster des empfangenden Fensters.
ON_NOTIFY_EX
Delegiert
die
Behandlung
einer
Steuerelementbenachrichtigung mit Zusatzdaten an eine Memberfunktion
der Klasse, die TRUE oder FALSE zurückliefert, um
anzuzeigen, dass die Benachrichtigung an ein anderes
Objekt zur weiteren Bearbeitung weitergeleitet werden soll.
ON_NOTIFY_EX_RANGE
Delegiert
die
Behandlung
einer
Gruppe
von
Steuerelementbenachrichtigungen
mit
Zusatzdaten,
repräsentiert durch einen Bereich von Kindbezeichnern, an
eine einzelne Memberfunktion der Klasse, Die TRUE oder
FALSE
zurückliefert,
um
anzuzeigen,
dass
die
Benachrichtigung an ein anderes Objekt zur weiteren
Bearbeitung weitergereicht werden soll. Die Steuerelemente,
die diese Benachrichtigung senden, sind Kindfenster des
empfangenden Fensters.
Außer diesen Makros gibt es noch mehr als 100 weitere Makros, die eine einzelne,
spezielle Meldung an eine Memberfunktion weiterleten. So delegiert z.B.
ON_CREATE die WM_CREATE-Meldung zu einer Funktion mit dem Namen
OnCreate().
Namen von Behandlungsfunktionen.
In der Fensterfunktion bleibt es dem Programmierer überlassen, wie er seine
Behandlungsmethode nennen will. In den Antworttabellen müssen spezielle Makros
verwendet werden. Für jede Nachricht ist der Name des Makros in der zugehörigen
Behandlungsmethode festgelegt.
Zur Ableitung des Makro-Namens für die Behandlungsmethode aus dem Namen der
Nachricht (z.B. WM_LBUTTONDOWN) stellt man dem Namen der Nachricht einfach
das Präfix ON_ voran (ON_WM_LBUTTONDOWN()). Der Name ergibt sich dann
aus dem Makronamen, indem man sämtliche Unterstriche und das Präfix WM
herausstreicht und nur noch die Anfangsbuchstaben der Silben groß schreibt
(OnLButtonDown()).
Die in den Makros festgelegten Namen können nicht verändert werden. Um Makros
in Meldungstabellen einzufügen, verwendet man am besten die Klassenansicht.
329
Die Programmiersprache C++
Empfang von Nachrichten.
Auch in MFC-Programmen werden Nachrichten von Windows an Fensterfunktionen
geschickt:
Handelt es sich um eine WM-Nachricht, werden spezielle Methoden der MFC
aufgerufen, die nachschauen, ob in dem Fenster, an das die Nachricht gerichtet ist,
eine Antworttabelle mit einem passenden Eintrag definiert ist. Wenn ja, wird die
zugehörige Behandlungsmethode aufgerufen.
Für COMMAND-Nachrichten (Menübefehle, Tastaturkürzel) kann die Nachricht sogar
innerhalb der Klassen des Anwendungsgerüsts weitergereicht werden.
Meldungen, die vom MFC-Code abgefangen werden
MFC-Klassen bearbeiten den größten Teil allgemeiner Meldungen, ohne dass dafür
eine Zeile Code geschrieben werden muß. Bspw. muß die Meldung, dass der
Anwender DATEI/SPEICHERN UNTER gewählt hat, nicht selbst abgefangen
werden. Die MFC fängt diese Meldung ab, ruft das ÖFFNEN-Dialogfeld zum
Einlesen des neuen Dateinamens auf, kümmert sich um alle im Hintergrund zu
erledigenden Aufgaben und ruft zum Speichern eine vom Anwender definierte
Funktion auf, die den Namen Serialize() tragen muß.
Gerätekontexte
Anstatt der API-Funktion GetDC() wird stets eine der MFC-Gerätekontextklassen
instantiiert.
Aufruf von API-Funktionen
Die MFC kapselt die am häufigsten benötigten API-Funktionen. In ihren Methoden
und Klassen. Für API-Funktionen, die auf Systemfunktionen zugreifen oder der
Shell-Programmierung dienen, gibt es allerdings keine Entsprechung in der MFC.
Darüber hinaus, gibt es zusätzlich zur Windows-API noch eine Reihe weiterer APIs
für spezielle Gebiete der Windows-Programmierung: die TAPI für TelefonieAnwendungen, die MAPI für Nachrichtenanwendungen, die DDI zum Schreiben von
Gerätetreibern.
Bei der Verwendung dieser API-Funktionen, ist zu beachten:
-
Voranstellen des Gültigkeitsbereichsoperator ::, damit der Compiler weiß, dass es sich um eine
globale Funktion und nicht eine Klassenmethode handelt.
Alle API-Funktionen verlangen als erstes den Handle des Objekts, auf das der Bezug stattfindet
API-Funktionen verwenden keine Defaultargumente.
API-Funktionen sind nicht überladen.
330
Die Programmiersprache C++
6.1.5 Erstellen einer Windows-Anwendung mit MFC
Eine MFC-Anwendung ruft niemals eine Windows-API-Funktion direkt auf. Statt
dessen konstruiert die Anwendung ein Objekt des entsprechenden Typs und nutzt
dessen Elementfunktionen. Der Konstruktor und der Destruktor überwachen die
Initialisierung sowie die Freigabe der Systemressourcen.
Klassenhierachie und Notation
Klassenhierachie
In der MFC findet man verschiedene Katagorien von Klassen.
1. Die Basisklasse CObject
Die überwiegende Anzahl der MFC-Klassen ist von der Basisklasse CObject abgeleitet, durch die die
Eigenschaften der Serialisierung und der Laufzeitinformation implementiert werden. Durch die
Serialisierung wird ein einfacher und sicherer Mechanismus zur Speicherung eines Objekts in eine
Datei und zum Laden aus einer Datei über ein Objekt der CArchiv-Klasse geschaffen.
Durch die Laufzeitinforamtion, die über die CRunTimeClass realisiert und durch Makros
DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC, DECLARE_DYNCREATE/IMPLEMENT_DYNCREATE oder
DECLARE_SERIAL/IMPLEMENT_SERIAL implementiert werden, wird die Feststellung zur
Klassenzugehörigkeit eines Objekts während der Laufzeit möglich.
2. Die Fensterunterstützungsklassen
Sie stellen einen Sammelbehälter für allgemeine Fenstertypen wie Rahmen-, Ansichts-, Dialogfenster
oder Steuerelemente zur Verfügung. Sie werden von der CWnd-Klasse abgeleitet, die eine große
Menge an Basisfunktionalitäten wie Initialiserung, Fensterzugriff, Größe und Position, Fenstertext,
Bildlauf, Menü, Zeitgeber, Nachrichten und OLE-Steuerelemente enthält.
Rahmenfensterklassen umfassen die Funktionalität der Hauptfenster der Applikation und der
Ansichten. Sie verwalten Menü- und Statusleisten sowie Werkzeugleistenschaltflächen. Dazu zählen
die SDI (Einzeldokument) und MDI (Multidokument) –Anwendungsrahmen, die alle von der
CFrameWnd-Klasse abgeleitet werden.
In Ansichtsklassen (von CView abgeleitet) werden gewöhnlich die Inhalte eines Dokuments in
verschiedenen Darstellungsarten sichtbar gemacht. Sie unterstützen den Bildlauf, Textbearbeitung,
Listenfelder, Formulare, Dialoge u.ä.
Dialogklassen (CDialog) enthalten die Funktionalität benutzerdefinierte Dialoge und
Standarddialoge.
Steuerelementklassen umfassen die Funktionalität der Windows-Steuerelemente wie Schaltflächen,
Textfelder,
Eingabefelder,
Listenfelder,
Kombinationsfelder,
Sinnbilder,
Kontrollkästchen,
Optionsfelder, Drehfelder, Statusanzeigen u.ä.
3. Applikationsunterstützungsklassen
Sie werden von der Basisklasse CCmdTarget abgeleitet. Sie können Nachrichen bearbeiten und
verfügen deshalb über eine Nachrichtentabelle. Da Fenster ebenfalls Nachrichten empfangen können
ist die Klasse CWnd von CCmdTarget abgeleitet.
Applikationsobjekte
(CWinApp)
repräsentieren
Prozesse
und
Threads.
Jede
MFCAnwendungdrahmenapplikation verfügt ein über CWinApp abgeleitetes Objekt, das die
Hauptnachrichtenschleife zur Verfügung stellt.
Dokumente (CDocument) repräsentieren in MFC-Applikationen Daten, die vom Anwender geöffnet,
bearbeitet und gespeichert werden können. Dokumente kooperieren mit Ansichten, in denen die
Datendargestellt und durch den Benutzer ninteraktiv bearbeitet werden.
Dokumentenvorlagen
(CDocTemplate)
beschreiben
das
grundlegende
Verhalten
von
benutzerdefinierten Dokumenten und Ansichten. Mit ihrer Hilfe werden Rahmenfenster, Dokument und
Ansicht zu einer Einheit verknüpft.
4. Grafikunterstützungsklassen
Sie umfassen die GDI-Objekte (CGdiObject) und die Gerätekontexte (CDC), die das Zeichnen auf
Ausgabegeräte mit hardwareunabhängigen Systemfunktionen ermöglichen.
Notation
331
Die Programmiersprache C++
- Klassennamen beginnen mit einem großen C (z.B. CObject).
- Membervariablen (Attribute) verwenden den Präfix m_ (z.B. m_lpszName)
- Funktions- und Methodennamen beginnen mit einem Großbuchstaben. Jedes Teilwort beginnt selbst
wieder mit einem Großbuchstaben. Das erste Wort ist in der Regel ein Verb (z.B.
GetWindowHandle).
- Microsoft empfiehlt die ungarische Methode zur Benennung von Variablen. Dem Namen wird ein
Typkürzel vorangestellt. Typkürzel können auch kombiniert werden (z.B. pszName, m_nWert)
Kürzel
b
c oder ch
dw
h
lpsz
n
p
w
wnd
l
cx, cy
clr
str
m_
sz
Datentyp
BOOL
char
DWORD
Handle
int
Pointer
WORD
CWnd
LONG
Horizontale bzw. vertikale Position
COLORREF
CString
Member-Variable
0-terminierter String
Abb.:
Eine sehr einfache Windows-Anwendung mit MFC
1. Erstellen eines Projekts mit einer Win32-Anwendung
2. Aufnahme der zwei folgenden Dateien über das Dialogfenster "Neues Element hinzufügen"
/* MyMFCAnw.h */
#include <afxwin.h>
class CMyMFCAnw:public CWinApp
{
public:
virtual BOOL InitInstance();
};
class CMainFrame:public CFrameWnd
{
public:
CMainFrame();
};
/* MyMFCAnw.cpp */
#include "MyMFCAnw.h"
CMyMFCAnw meineAnw;
BOOL CMyMFCAnw::InitInstance()
{
m_pMainWnd = new CMainFrame;
m_pMainWnd->ShowWindow(m_nCmdShow);
return TRUE;
}
332
Die Programmiersprache C++
CMainFrame::CMainFrame()
{
Create(NULL,"Programm MyMFCAnw");
}
3. Öffnen des Dialogfensters "Eigenschaftsseiten"
(z.B. durch Klick mit der rechten Maustaste auf das Projektverzeichnis, dann Klick mit der linken
Maustaste im resultierenden Menü auf den Menüpunkt "Eigenschaften").
4. Einstellen der Projekteigenschaften nach der vorliegenden Vorgabe.
5. Anschließend Klick auf OK im Dialogfenster "Eigenschaftsseiten".
6. Das Projekt kann anschließend erstellt und zum Laufen gebracht werden. Es erzeugt ein leeres
Rahmenfenster.
MFC-Programme müssen die Header-Datei afxwin.h einbinden. Dies ist eine Datei
mit mehreren tausend Zeilen, die selbst noch andere große Dateien inkludiert (z.B.
windows.h). Ein MFC-Programm muß mindestens zwei Klassen deklarieren und je
ein Objekt der folgenden Klassen erzeugen:
-
Ein Objekt der Applikationsklasse repräsentiert das eigentliche Programm, diese Klasse
(CMyMFCAnw) muß von der Basisklasse CWinApp abgeleitet werden.
Ein Objekt einer Fensterklasse für das Hauptfenster fungiert als Rahmenfenster, die Klasse
(CMainFrame) kann z.B. von der Basisklasse CFrameWnd abgeleitet werden.
Von der Applikationsklasse wird genau eine globale Instanz erzeugt (meineAnw ).
Globale Instanzen werden vor der Abarbeitung des Hauptprogramms erzeugt, so
dass der Konstruktor von CWinApp die ersten Aktionen des Programms ausführt,
verschiedene Windows-Variablen werden initialisiert, einer globalen Variablen wird
333
Die Programmiersprache C++
der Zeiger auf die Instanz meineAnw zugewiesen. Auf diesen Zeiger kann bei Bedarf
mit der ebenfalls globalen Funktion AfxGetApp() zugegriffen werden.
Anschließend startet die (in den Basisklassen versteckte) Funktion WinMain. Sie
greift auf die Methoden der Applikationsklasse CMiniMFCApp zu. Die wichtigsten
von WinMain zu startenden Methoden der Anwendungsklasse übernehmen
folgende Aufgaben:
-
-
Die Methode InitInstance sollte grundsätzlich von der Applikationsklasse des Programms
überladen werden, da in der CWinApp-Version kein Fenster erzeugt wird. InitInstance ist der
geeignete Ort, um Parameter der Applikation zu initialiseren, um das Hauptfenster zu erzeugen
und auf den Bildschirm zu bringen.
Die CWinApp-Methode Run betreibt die Nachrichtenschleife. Die Nachrichtenschleife bricht beim
Eintreffen der WM_QUIT-Botschaft ab und startet die Methode ExitInstance, die sich für
Aufräumarbeiten anbietet und für einen solchen Fall überladen werden müsste.
Im vorliegenden Beispiel erzeugt die Methode InitInstance ein Objekt der
Fensterklasse CMainFrame, deren Adresse in der Variablen m_pMainWnd abgelegt
wird. Mit dem Zeiger m_pMainWnd kann man auf alle Methoden der Fensterklasse
zugreifen. Mit der Methode ShowWindow wird das Fenster auf den Bildschirm
gebracht.
Mit der Methode Create von CMainFrame wird der Name der Fensterklasse (hier
NULL) und der Fenstertitel gesetzt.
Erweiterung der einfachen Windows-Anwendung mit MFC durch Nachrichtenschleife
und eine neue Nachrichtenbehandlungsmethode
Die
Erweiterungen
betreffen
die
Nachrichtenschleife
und
die
neue
Nachrichtenbehandlungsmethode, mit der der Text "Hallo, MFC-Welt!" in den
Clientbereich geschrieben wird.
/* */
#include <afxwin.h>
class CMyMFCAnw:public CWinApp
{
public:
virtual BOOL InitInstance();
};
class CMainFrame:public CFrameWnd
{
public:
CMainFrame();
protected:
afx_msg void OnPaint();
// behandelt die WM_PAINT-Botschaft
DECLARE_MESSAGE_MAP()
// übernimmt die Deklaration von Daten und Methoden
// der Message Map
};
/* … */
#include "MyMFCAnw.h"
CMyMFCAnw meineAnw;
334
Die Programmiersprache C++
BOOL CMyMFCAnw::InitInstance()
{
m_pMainWnd = new CMainFrame;
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
// Das Neuzeichnen des Fensters wird durch UpdateWindow
// infolge OnPaint ausgelöst
return TRUE;
}
// Von den drei folgenden Makros wird Code für die Zuordnung
// der Botschaften (WM_PAINT) zu ihren Behandlungsroutinen
// erzeugt
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_PAINT()
END_MESSAGE_MAP()
CMainFrame::CMainFrame()
{
Create(NULL,"Programm MyMFCAnw");
}
void CMainFrame::OnPaint()
{
CPaintDC dc(this);
CRect rect;
GetClientRect(&rect);
dc.DrawText("Hallo, MFC-Welt!", &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}
335
Die Programmiersprache C++
6.2 Die zentralen MFC-Klassen (Zusammensetzung und
Zusammenspiel)
6.2.1 Übersicht zu den zentralen MFC-Klassen
Ein MFC-Programm besteht mindestens aus einer von der Anwendungsklasse
CWinApp abgeleiteten Klasse. Sie besorgt die Programmabarbeitung jedoch nicht
die Fenstererstellung. Im Programm-Code wird die CWinApp-Methode
InitInstance() mit der zur Fenstererzeugung und –anzeige nötigen Befehle
überladen. Es gibt daher auch keine WinMain()-Funktion mehr.
Die Klasse CWnd ist die Basisklasse aller Fenster. Von ihr leiten sich alle weiteren
wichtigen Fensterklassen ab:
- CFrameWnd (Rahmenfenster)
- CView (Unterschiedliche Ansichten)
- CDialog (Common Dialog Boxes, eigene Dialoge)
- CDocuments (Dokumente)
- CDocTemplates (Dokumenten-Templates)
- CMDIChildWnd(MDI-Kind)
- CMDIFrameWnd (MDI-Parent)
- Controls (CAnimateCtrl, CButton, CComboBox, CEdit, CHeaderCtrl, CHotKeyCtrl, CListBox,
CListCtrl, CProgressCtrl, COleControl, CRichEditCtrl, CScrollBar, CSliderCtrl, CSpinButtonCtrl,
CStatic, CStatusBarCtrl, CTabCtrl, CToolBarCtrl)
In der Klasse CWnd setzt der Mechanismus zur Abarbeitung der Nachrichten in einer
Fensterfunktion an. Er wird mit Hilfe sog. Message Maps und zugehöriger Makros
realisiert.
Die Member-Funktionen der Klasse CWnd unterteilen sich in folgende Funktionen:
Fenster-Initialisierung und -Erzeugung
Funktionen zur Abfrage des Fensterstatus
Abfrage und Veränderung von Fenstergröße und -position
Fenster-Identifikation
Update- und Zeichenfunktionen
Koordinaten-Mapping
Manipulation des Systemmenüs
Manipulation von Dialog Box Items
Manipulation von Menüs
Setzen und Löschen von Zeitgebern (Timer)
Alarmfenster (messageBox)
Window-Messages
Manipulation des Clipboard-Inhalts
Die Klasse CFrameWnd (abgeleitet von CWnd) besitzt alle Eigenschaften eines
normalen Popup-Windows. Es kann somit als Hauptfenster der Anwendung dienen.
Es sind darin keine weiteren Funktionalitäten realisiert.
Weitere von CWnd abgeleitete Klassen machen es auf einfache Art möglich, weitere
grafische Gestaltungselemente in ein Fenster einzuführen:
CToolBar
CStatusBar
CDialogBar
CSplitterWnd (geteiltes Fenster)
336
Die Programmiersprache C++
Zur Erzeugung und Verwaltung von Klassen wird die Klasse CDialog genutzt. Man
unterscheidet auch bei diesen Dialogen zwischen modal und nicht modal. Von
CDialog ausgehend sind weitere Sonderklassen definiert, die in der Datei
COMMDLG.DLL für alle Windowsprogrammierer vorgegeben sind:
- CFileDialog
- CColorDialog
- CFontDialog
- CFindReplaceDialog
Die Ausgabe von Text und Zeichnung erfolgt über den Gerätekontext. In der MFC
stammen diese von der Klasse CDC ab. Abgeleitete Kontexte hiervon sind
- CClientDC
- CWindowDC
- CPaintDC
- CMetaFileDC
Zur Ausgabe von Grafiken wurden Klassen mit den wichtigsten Werkzeugen
definiert:
- CPen
- CBrush
- CFont
- CBitMap
- CPalette
- CRgn
Die MFC stellt auch viele Funktionen und Klassen zur Verfügung, die die
Handhabung von Documents und View vereinfachen. Damit können Dokumente
durch mehrere von ihnen abhängige Ansichten dargestellt werden. Die beteiligten
Klassen sind:
- CDocument
- CDocTemplate
- CView
6.2.2 Allgemeine Ereignisbehandlung in Windows mit der MFC
Prizipieller Ablauf
1. Das Betriebssystem erkennt ein Ereignis (Mausklick, Tastaturbetätigung, etc. ).
2. Ermittlung des Fensters, für das das Ereignis bestimmt ist.
3. Aufbau einer speziellen Funktion der zugehörigen Anwendung. Die Parameter der Funktion
beschreiben exakt das Ereignis.
4. Arbeitsweise der Funktion
- Ermittlung des Ereignistyps anhand der Parameter
- Aufruf der zum Ereignistyp gehörigen Ereignisfunktion
5. Die Ereignisfunktion führt enwendungsspezifische Aktionen aus (z.B. das Zeichnen einer Linie)
Bedingungen
337
Die Programmiersprache C++
-
Das Betriebssystem muß die Position und Größe aller Fenster sowie die zugehörigen Programme
kennen
Die Anwendung muß wissen, welche Funktionen bei einem bestimmten Ereignis aufgerufen
werden sollen.
6.2.3 Verschiedene spezielle MFC-Klassen
6.2.3.1 Spezielle Klassen für die Textverarbeitung
Textverarbeitung gehört zu den wichtigsten und am häufigsten benötigten Aufgaben
der Windows-Programmierung. Entsprechend vielseitig ist das Angebot an
unterstützenden Klassen.
1. Meldungsfenster (Einfachster Weg zur Ausgabe eines kurzen Textes)
Sie sind in Windows vordefiniert und können durch den Aufruf einer einzigen
Methode erzeugt und angezeigt werden.
int CWnd::Messagebox(LPCTSTR lpszText,LPCTSTR lpszCaption=Null,
UINT nType=MB_OK);
lpszText .... Text der ausgegeben werden soll
lpszCaption . Text für den Titel des Meldungsfensters
nType ....... Kombination aus Konstanten, die angeben, welche Schalter und
welches Symbol im Meldungsfenster angezeigt werden soll.
Schalter
MB_OK
MB_OKCANCEL
MB_RETRYCANCEL
MB_YES
MB_YESNO
MB_YESNOCANCEL
Symbole
MB_ICONEXCAMATION
MB_ICONINFORMATION
MB_ICONQUESTION
MB_ICONSTOP
Abb.: Konstanten für den nType-Parameter
Die MessageBox() ist eine Methode der Fensterklasse CWnd und ist nur in
Methoden von Fensterklassen verfügbar. Zur Anzeige von Meldungsfenstern aus
den Methoden anderer Klassen verwendet man die globale Funktion
AfxMessageBox().
int AfxMessageBox(LPCTSTR lpszText,UINT nZype = MB_OK,UINT nIDHelp=0)
Der Rückgabewert der Methoden zeigt an, welcher Schalter zum Schließen des Meldungsfensters
gedrückt wurden.
Rückgabewert
IDABORT
IDCANCEL
IDIGNORE
IDNO
IDOK
Zeigt an:
Die Abbrechen-Schaltfläche wurde gedrückt
Die Abbrechen-Schaltfläche wurde gedrückt
Die Ignorieren-Schaltfläche wurde gedrückt
Die Nein-Schaltfläche wurde gedrückt
Die OK-Schaltfläche wurde gedrückt
338
Die Programmiersprache C++
IDRETRY
IDYES
Die Wiederholen-Schaltfläche wurde gedrückt
Die Ja-Schaltfläche wurde gedrückt.
Abb.:
Die Funktion AfxMessageBox() ist keiner MFC-Klasse zugeordnet. Dies kann man
ausnutzen und ein Programm erstellen, das mit weniger als 10 Zeilen Quellcode ein
Fenster (zählt man den OK-Button mit, sind es sogar zwei) ausgibt:
#include <afxwin.h>
class CMeineAnwendung : public CWinApp
{
virtual BOOL InitInstance()
{
AfxMessageBox("Hallo Welt !!!");
return TRUE;
}
};
CMeineAnwendung meineAnwendung;
2. Text zeichnen (in ein Ansichts- oder ein Rahmenfenster)
Falls man einen passenden Gerätekontext zu einem Fenster beschafft hat, stehen
zwei Gerätekontextmethoden für die Textausgabe zur Verfügung.
CDC::TextOut()
CDC::DrawText()
3. Steuerelemente
Steuerelemente können für die Ein- und Ausgabe für Text verwendet werden. Die
grundlegende Funktionalität des Steuerelements ist bereits in Windows
implementiert.
CStatic-Textfeld.
CEdit-Eingabefeld
CRichEdit-Eingabefeld
4. Spezielle Ansichtsklassen
Die MFC stellt zwei abgeleitete View-Klassen bereit, die das Erscheinungsbild und
Verhalten eines normalen Ansichtsfensters mit der Funktionalität eines
Eingabesteuerelements verbinden.
339
Die Programmiersprache C++
CEditView
CRichEditView
5. Die Klasse Klassen CString (MFC-Stringklasse)
Sie erleichtert die Handhabung von Zeichenketten, insbesondere die damit
verbundene dynamische Speicherverwaltung. Sie soll die alten C-Zeichenarrays
ersetzen und bietet mächtige und flexible CString-Operationen.
Sammlungen (Container) von Strings können in den MFC-Klassen CStringArray
oder CStringList verwaltet werden.
6.2.3.2 Dateibearbeitung
Die MFC-Bibliothek stellt die Klasse CFile für Dateizugriffe bereit. CFile
unterstützt nur binäre, ungepufferte Operationen. Die abgeleitete Klasse
CStudioFile ermöglicht gepuffertes Lesen und Schreiben von Binär- und
Textdateien. Alterantiv können die ANSI-C++-Datenstreamklassen verwendet
werden.
In C/C++ benutzt man für das Speichern bzw. Laden aus Dateien die in den
Standardbibliotheken definierten Behandlungsfunktionen (fopen(), fprintf(),
usw.) oder Streams (fstream). Diese besitzen auch in Windows-Programmen ihre
Gültigkeit. Windows stellt aber auch eigene Funktionen für die Arbeit mit Dateien zur
Verfügung. In der MFC sind diese in der Klasse CFile gekapselt.
CFile ist die Basisklasse für MFC-Dateidienste. CFile unterstützt das Lesen und
Schreiben in (binäre) Dateien:
Dateien öffnen. Das Öffnen kann mit Hilfe der folgenden Konstruktoren erfolgen:
CFile(int hFile);
CFile(LPCTSTR lpszFileName,UINT nOpenFlags);
Konstante
CFile::modeCreate
Bedeutung
Datei wird bei Bedarf neu angelegt. Der Inhalt bestehender Dateien wird
beim Öffnen gelöscht.
CFile::modeRead
Die Datei kann nur gelesen werden
CFile::modeReadWrite Die Datei wird zum Lesen und Schreiben geöffnet
CFile::modeWrite
Die Datei wird nur zum Schreiben geöffnet
Abb.: Einige Konstanten für den nOpenFlags-Parameter
Alternativ kann
1. der Konstruktor CFile() verwendetwerden, dem keine Parameter übergeben werden.
2. danach die Methode Open() aufgerufen werden.
Schließen von Dateien. Zum Schließen von Dateien wird die Methode Close()
aufgerufen.
Lesen und Schreiben in ein CFile-Objekt. Es wird mit den Methoden Read() und
Write() ausgefüht:
340
Die Programmiersprache C++
virtual UINT Read(void* lpBuf, UINT nCount);
lpBuf … bezeichnet einen Speicherbereich (Puffer), in den die Daten eingelesen werden.
nCount ... ist die Anzahl Bytes, die maximal eingelesen werden.
virtual void Write(const void* lpBuf, UINT nCount);
lpBuf …
nCount ...
ist ein Zeiger auf den Speicherbereich, in dem die auszugebenden Daten stehen.
ist die Anzahl Bytes, die ausgegeben werden.
Fehlerbehandlung. Einige CFile-Methoden (z.B. Open()) zeigen aufgetretene
Fehler in ihren Rückgabewerten an. Andere Funktionen lösen eine Ausnahme vom
Typ CFileException aus.
6.2.3.3 Serialisierung
Als Serialisierung bezeichnet man das Lesen und Schreiben von Objekten (genauer:
CObject-Objekten).
CObject ist die oberste Basisklasse der MFC-Klassen. Alle Objekte von Klassen
(MFC-Klassen und vom Programmierer selbst definierte Klassen) können mit Hilfe
der Serialisierung in Dateien geschrieben oder aus Dateien gelesen werden. Zum
Schreiben von Objekten in CFile-Dateien per Serialisierung braucht man einen
Mittler: die CArchive-Klasse. Diese Klasse dient als Eingabe- bzw. AusgabeStream für ein CFile-Objekt.
6.2.3.3 CTime
CTime-Klassenobjekte geben das absolute Datum und die Uhrzeit an. Hierbei
kapselt sie den ANSI-Datentyp time_t und seine verwandten Laufzeitfunktionen,
einschl. der Fähigkeit in die gregorianische Zeitrechnung sowie 24-StundenAnzeigen umzurechnen. CTime-Werte basieren auf UCT (Universal Coordinate Time
oder Greenwich Mean Time), wobei die lokale Zeitzone durch die TZUmgebungsvariable identifiziert wird.
341
Die Programmiersprache C++
6.3 Die Kernkomponenten
6.3.1 Aufbau von Dialogen aus Steuerelementen
Zu Windows gehören verschiedene Standardsteuerelemente, z.B. Schaltflächen,
Schieberegler, Kontrollkästchen (Check Box), Kombinationsfeld (Combo Box) auch
als Drop-Down-Listenfeld bezeichnet
Zur Aufnahme von Steuerelementen (Controls) in einem Rahmen oder
Ansichtsfenster verweigert die IDE jegliche Unterstützung. Steuerelemente müssen
manuell konfiguriert werden und durch Angaben von Koordinaten im Quelltext
plaziert werden.
Steuerelemente sind Windows-Fenster. Zahlreiche Steuerelemente sind in Windows
vordefiniert und als Teil des Betriebssystems implementiert. Zu diesen StandardSteuerelementen zählen:
Das statische Textfeld
Das Eingabefeld
Die Schaltfläche
Die Kontrollkästchen und Optionsfelder
Die Listen- und Kombinationsfelder
Die MFC stellt Klassen bereit, die diese Steuerelemente kapseln. Für alle wichtigen
Arbeiten im Umgang mit Steuerelementen sind in diesen Klassen passende
Methoden definiert.
Eigenschaften
-
-
Steuerelemente (Controls) haben jeweils eigene Fenster
Controls werden an einer angegebenen Position mit rechteckiger Bounding-Box erzeugt.
Controls erhalten bei ihrer Erzeugung eine Reihe von Stilelementen, die ihr Aussehen und
Verhalten bestimmen
Controls haben eine eigene Ereignisbehandlungsmethode.
- Nach einem Ereignis des Betriebssystems ändert das Control in der Regel seinen Zustand
- Danach wird das Ereignis weitergemeldet.
Controls sind Objekte von Klassen und haben dementsprechend eigene Methoden zum Lesen und
Schreiben der Zustände.
Fenster
Die folgenden Stile gelten für fast alle Controls (, weil sie sich auf die Fensterklasse CWnd beziehen):
WS_CHILD, WS_VISIBLE, WS_GROUP. Soll ein Steuerelement mehr als einen Stil erhalten, so
werden alle Stile mit logischem Oder "|" verknüpft.
Einige wichtige Methoden der Basisklasse CWnd sind:
void SetWindowText(LPCSTR text)
LPCSTR GetWindowText() const
schreibt bzw. liest Beschriftung (z.B. für Buttons, TextFelder, Labels, ...)
void setFont(CFont* font, BOOL hRedraw = TRUE)
CFont* GetFont() const
schreibt bzw. liest den Zeichensatz, mit dem die Beschriftung dargestellt wird.
Manuelle Erzeugung von Steuerelementen
-
Ein Objekt wird als Attribut der entsprechenden Dialogklasse angelegt, z.B. CButton okButton
342
Die Programmiersprache C++
-
Während der Konstruktion des Dialogs (z.B. im Konstruktor der Dialogklasse) wird die Methode
Ctreate des Controls zur Konfigurierung aufgerufen: okButton.Create("OK",WS_CHILD | WS_VISIBLE
| BS_PUSHBUTTON, CRect(CPoint(10,10),CSize(125,5)),this,IDC_BUTTON_OK)
-
Die Controls werdem in absoluten Koordinaten innerhalb des Clientbereichs positioniert.
Jedes Control erhält eine eindeutige Identifikationsnummer (ID). Die ID wird in der Regel in der
Header-Datei als Präprozessor-Direktive festgelegt.
Aufbau der ID-Namen (alle Teile sind mit einem Unterstrich _ verbunden.
-- IDC
-- Typ des Controls, z.B. BTN für Button, STC für Static
-- Bezeichnung, die beim Entwickler eine gewisse Assoziation mit der Bedeutung herstellt, z.B.
IDC_BTN_OK.
Ereignisbehandlung
-
Da ein Control seine Ereignisse an sein Vaterobjekt weiterleitet, werden die für den Anwender
interessanten Ereignisse dort behandelt.
Deklaration eines entsprechenden Makros für den Ereignistyp in der Message-Map.
-- Makro-Name: Ereignistyp
-- 1. Parameter: ID des Controls
-- 2. Parameter: Aufzurufende Methode
Bsp.: Eine MFC-Anwendung mit manueller Erstellung von Steuerelementen 176. Die
Anwendung soll folgendes Dialogfenster erzeugen und je nach Button-Aktivierung
einen entsprechenen Strich in das Fenster zeichnen:
1. Erstellen leeres Projekt
-
Über Datei | Neu | Projekte "Win32-Anwendung" markieren
Angabe des Projektnamens
Erstellen eines leeren Projekts
2. Einfügen einer neuen Header-Datei "MeineAnwendung.h"
// Header-Datei des Hauptfensters
#include <afxwin.h>
class CMeineAnwendung : public CWinApp
{
public:
virtual BOOL InitInstance();
};
class CHauptfenster : public CFrameWnd
{
CButton btnBlack;
CButton btnRed;
CButton btnChkDotted;
CPen penRed;
CPen penRedDotted;
176
vgl. pr63210
343
Die Programmiersprache C++
CPen penBlackDotted;
char aktFarbe;
// R = rot, B = schwarz
public:
CHauptfenster();
// Ereignisse
afx_msg void OnPaint();
// Control-Ereignisse
afx_msg void OnClickBlack();
afx_msg void OnClickRed();
afx_msg void OnClickDotted();
DECLARE_MESSAGE_MAP()
};
3. Einfügen einer neuen Quellcode-Datei "MeineAnwendung.cpp"
# include "MeineAnwendung.h"
// Erzeuge Anwendungsobjekt
static CMeineAnwendung meineAnwendung;
//
BOOL CMeineAnwendung :: InitInstance()
{
m_pMainWnd = new CHauptfenster;
m_pMainWnd->ShowWindow(m_nCmdShow);
return TRUE;
}
/* Implementierung des Hauptfensters */
// Control-IDs
#define IDC_BTN_BLACK
199
#define IDC_BTN_RED
200
#define IDC_BTN_DOTTED 201
// Message-Map
BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd)
ON_WM_PAINT()
// Beim Click auf den zugehoerigen Button
// wird diese Funktion ausgefuehrt
ON_BN_CLICKED(IDC_BTN_BLACK,OnClickBlack)
ON_BN_CLICKED(IDC_BTN_RED,OnClickRed)
ON_BN_CLICKED(IDC_BTN_DOTTED,OnClickDotted)
END_MESSAGE_MAP()
/* Erzeugen des Hauptfensters und der Controls */
CHauptfenster :: CHauptfenster()
{
// 1. Schritt: Erzeuge das Hauptfenster
Create(NULL,"Push & Check Schaltflaechen",
WS_OVERLAPPEDWINDOW,CRect(0,0,300,140));
// 2. Schritt: Erzeuge die Fenster zu den Steuerelementen
btnBlack.Create("Schwarze Linie",
WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON,
CRect(CPoint(5,5),CSize(125,25)),this,IDC_BTN_BLACK);
btnRed.Create("Rote Linie",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
CRect(CPoint(5,35),CSize(125,25)),this,IDC_BTN_RED);
btnChkDotted.Create("Dotted Linie",
WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
CRect(CPoint(5,65),CSize(125,25)),this,IDC_BTN_DOTTED);
// 3. Schritt: Erzeuge Pens
penRed.CreatePen(PS_SOLID,1,RGB(255,0,0));
penRedDotted.CreatePen(PS_DOT,1,RGB(255,0,0));
penBlackDotted.CreatePen(PS_DOT,1,RGB(0,0,0));
// 4. Schritt: Dateninitialisierung
aktFarbe = 'B';
}
344
Die Programmiersprache C++
// Einer der beiden Pushbuttons wurde gedrueckt
void CHauptfenster :: OnClickBlack()
{
aktFarbe = 'B';
Invalidate();
}
void CHauptfenster :: OnClickRed()
{
aktFarbe = 'R';
Invalidate();
}
// Neuzeichnen nach Ereignis, gestartet durch Invalidate()
void CHauptfenster :: OnPaint()
{
CPaintDC dc(this);
if (aktFarbe == 'R')
{
if (btnChkDotted.GetCheck() == BST_UNCHECKED)
dc.SelectObject(&penRed);
else
dc.SelectObject(&penRedDotted);
}
else {
if (btnChkDotted.GetCheck() == BST_UNCHECKED)
dc.SelectStockObject(BLACK_PEN);
else
dc.SelectObject(&penBlackDotted);
}
dc.MoveTo(CPoint(150,10));
dc.LineTo(CPoint(240,80));
}
// Checkbox gedrueckt: Neuzeichnen
void CHauptfenster :: OnClickDotted()
{
Invalidate();
}
6.3.2 Die wichtigsten Steuerelemente
Schaltflächen (Buttons)
Eine Schaltfläche (Button) kann verschiedene Ausprägungen haben: Checkbox,
PushButton, Radio-Button, Groupbox.
PushButton
Über einen PushButton löst der Benutzer eine bestimmte Aktion aus. Die
Beschriftung (Caption) der Schaltfläche sollte einen Hinweis auf die Aktion liefern,
die beim Klicken auf die Schaltfläche ausgeführt wird. Die wichtigsten Eigenschaften,
Stile und Nachrichten für den PushButton sind:
Eigenschaft
ID
Beschriftung
Sichtbar (Visible)
Deaktiviert (Disabled)
Default Button
Beschreibung
identifiziert das Steuerelement
gibt den auf der Schaltfläche angezeigten Text an
gibt an, ob das Steuerelement sichtbar ist, wenn die Anwendung läuft.
gibt an, dass das Steuerelement deaktiviert ist.
gibt an, dass dieses Steuerelement ausgelöst werden soll, wenn der
Benutzer die Eingabetaste betätigt.
345
Die Programmiersprache C++
Tabstopp
gibt an, ob Benutzer das Steuerelment beim Navigieren mit der
tabulatortaste erreichen können.
Abb. Wichtige Eigenschaften des Steuerelements PushButtom
Stile
BS_PUSHBUTTON
BS_DEFPUSHBUTTON
Nachrichten
ON_BN_CLICKED
ON_BN_DOUBLECLICKED
Abb.: Stile und Nachrichten des Pushbutton
CButton. Das Steuerelement PushButton ist in der MFC-KLasse CButton verkapselt.
Die Klasse ist ein Abkömmling der Klasse CWnd und verkapselt nicht nur PushButton
sondern auch das Kontrollkästchen und das Optionsfeld.
Checkbox
Stile
BS_CHECKBOX
BS_AUTOCHECKBOX
BS_3STATE
BS_AUTO3STATE
BS_LEFTTEXT
Nachrichten
ON_BN_CLICKED
ON_BN_DOUBLECLICKED
Radio Button
Stile
BS_RADIOBUTTON
BS_AUTORADIOBUTTON
-
Nachrichten
ON_BN_CLICKED
ON_BN_DOUBLECLICKED
Radio Buttons werden (in der Regel) zu einer Gruppe zusammengefasst.
Nur ein Button der Gruppe kann selektiert werden
In der MFC müssen Buttons einer Gruppe direkt aufeinanderfolgende Identifikationsnummern
besitzen
Das erste Control der Gruppe muß den Stil WS_GROUP gesetzt haben
Dabei darf es sich auch um eine Group-Box handeln
Es gibt zwei wesentliche Methoden zum Umgang mit gruppierten Buttons:
-- CheckRadioButton(UINT FirstID, UINT LastID,UINT ToBeChecked)
Der Button mit der ID ToBeChecked wird ausgewählt, alle anderen in dem angegebenen Bereich
deselektiert.
-- UINT CtrlID = GetCheckRadioButton(UINT FirstCtrl, UINT LastCtrl)
Es wird die ID des selektierten Buttons eines angegebenen Bereichs ausgelesen
Groupbox
Stile
BS_GROUPBOX
Nachrichten
Statische Textfelder (Static)
Static ist ein Label, das Text oder ein Icon beinhalten kann.
Stile
SS_SUNKEN
SS_CENTER
SS_LEFT
Nachrichten
ON_STN_CLICKED
ON_STN_DBLCLICK
ON_STN_ENABLE
346
Die Programmiersprache C++
SS_RIGHT
SS_NOTIFY
SS_ICON
ON_STN_DISABLE
Ein- oder mehrzeiliges Texteingabefeld
Stile
WS_BORDER
ES_LEFT
ES_CENTER
ES_AUTOSCROLL
ES_MULTILINE
Nachrichten
EN_SETFOCUS
EB_KILLFOCUS
EN_CHANGE
Bsp.: Ereignisse in TextEingabefeldern
Die Anwendung soll folgendes Dialogfenster erzeugen und nach Eingaben in den
Eingabetextfeldern eine Berechnung ausführen.
1. Erstellen leeres Projekt
-
Über Datei | Neu | Projekte "Win32-Anwendung" markieren
Angabe des Projektnamens
Erstellen eines leeren Projekts
2. Einfügen einer neuen Header-Datei "MeineAnwendung.h"
#include <afxwin.h>
class CMeineAnwendung : public CWinApp
{
public:
virtual BOOL InitInstance();
};
class CHauptfenster : public CFrameWnd
{
CStatic stcBreite;
CEdit edtBreite;
CStatic stcLaenge;
CEdit edtLaenge;
CStatic stcFlaeche;;
CStatic stcResult;
CButton btnCalc;
void Recalculate();
public:
CHauptfenster();
// Control-Ereignisse
afx_msg void OnClickCalc();
afx_msg void OnLaengeFocus();
afx_msg void OnLaengeKillFocus();
afx_msg void OnBreiteChange();
DECLARE_MESSAGE_MAP()
347
Die Programmiersprache C++
};
3. Einfügen einer neuen Quellcode-Datei "MeineAnwendung.cpp"
# include "MeineAnwendung.h"
// Erzeuge Anwendungsobjekt
static CMeineAnwendung meineAnwendung;
//
BOOL CMeineAnwendung :: InitInstance()
{
m_pMainWnd = new CHauptfenster;
m_pMainWnd->ShowWindow(m_nCmdShow);
return TRUE;
}
/* Implementierung des Hauptfensters */
// Control-IDs
#define IDC_BTN_CALC
300
#define IDC_STC_BREITE
310
#define IDC_EDT_BREITE
311
#define IDC_STC_LAENGE
320
#define IDC_EDT_LAENGE
321
#define IDC_STC_FLAECHE 330
#define IDC_STC_RESULT
331
// Message-Map
BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd)
ON_BN_CLICKED(IDC_BTN_CALC,OnClickCalc)
ON_EN_SETFOCUS(IDC_EDT_LAENGE,OnLaengeFocus)
ON_EN_KILLFOCUS(IDC_EDT_LAENGE,OnLaengeKillFocus)
ON_EN_CHANGE(IDC_EDT_BREITE,OnBreiteChange)
END_MESSAGE_MAP()
/* Erzeugen des Hauptfensters und der Controls */
CHauptfenster :: CHauptfenster()
{
// 1. Schritt: Erzeuge das Hauptfenster
Create(NULL,"Static & Edit Controls",
WS_OVERLAPPEDWINDOW,CRect(0,0,300,140));
// 2. Schritt: Erzeuge static controls
stcLaenge.Create("Laenge",
WS_CHILD | WS_VISIBLE | SS_CENTER,
CRect(CPoint(10,10),CSize(60,25)),this,IDC_STC_LAENGE);
stcBreite.Create("Breite",
WS_CHILD | WS_VISIBLE | SS_CENTER,
CRect(CPoint(80,10),CSize(60,25)),this,IDC_STC_BREITE);
stcFlaeche.Create("Flaeche",
WS_CHILD | WS_VISIBLE | SS_CENTER,
CRect(CPoint(150,10),CSize(120,25)),this,IDC_STC_FLAECHE);
stcResult.Create("",
WS_CHILD | WS_VISIBLE | SS_SUNKEN | SS_CENTER,
CRect(CPoint(150,40),CSize(120,25)),this,IDC_STC_RESULT);
// 3. Schritt: Erzeuge edit und andere controls
edtLaenge.Create(WS_CHILD | WS_VISIBLE | WS_BORDER,
CRect(CPoint(10,40),CSize(60,25)),this,IDC_EDT_LAENGE);
edtBreite.Create(WS_CHILD | WS_VISIBLE | WS_BORDER,
CRect(CPoint(80,40),CSize(60,25)),this,IDC_EDT_BREITE);
btnCalc.Create("Calc Area", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON,
348
Die Programmiersprache C++
CRect(CPoint(60,70),CSize(100,25)),this,IDC_BTN_CALC);
edtLaenge.SetFocus();
}
// Interne Methode zum erneuten Rechnen und Ausgeben von Werten
void CHauptfenster :: Recalculate()
{
CString str;
edtLaenge.GetWindowText(str);
double laenge = atof(str);
edtBreite.GetWindowText(str);
double breite = atof(str);
double flaeche = laenge * breite;
str.Format("%f",flaeche);
stcResult.SetWindowText(str);
}
// Ereignisbehandlung
void CHauptfenster :: OnLaengeFocus()
{
edtLaenge.SetWindowText("");
stcResult.SetWindowText("");
}
void CHauptfenster :: OnLaengeKillFocus()
{
CString str;
edtLaenge.GetWindowText(str);
if (atof(str) < 0)
{
MessageBox("Unzulaessiger Eintrag");
edtLaenge.SetWindowText("");
edtLaenge.SetFocus();
}
}
void CHauptfenster :: OnClickCalc()
{
Recalculate();
}
void CHauptfenster :: OnBreiteChange()
{
Recalculate();
}
349
Die Programmiersprache C++
6.4 Erstellen von Dialogen über Ressourcen
6.4.1 Ressourcen
Ressourcenkonzept
Die Grundidee des Ressourcenkonzepts ist die Auslagerung bestimmter Elemente
der grafischen Benutzeroberfläche (Dialoge, Menüs, Bitmaps, etc.). Auslagerung
bedeutet: Die Elemente werden nicht im Quelltext, sondern in Dateien definiert. Die
Verbindung von C++-Quelltextdatei zu einer externen Ressourcendatei ist die
Ressourcen-ID.
Ressourcenskriptdatei
Ressourcen werden in sog. Ressourcenskriptdateien definiert (Extension .rc). bei
diesen Dateien handelt es sich um einfache Textdateien, in denen die
verschiedenen Ressourcen nach eigenen Syntayregeln definiert werden.
RES-Datei
Ressourcenskriptdateien können nicht vom C-Compiler übersetzt werden, sie
bedürfen eines eigenen Ressourcen-Compilers177. Dieser erzeugt aus der
Ressourcenskriptdatei eine binäre .res-Datei. Ressourcenskriptdateien in einem
Projekt werden über die Befehle im Menü ERSTELLEN beim Kompilieren durch
automatischen Aufruf des Ressourcen-Compilers in eine .res-Datei übersetzt. Bei
der Projekterstellung wird diese binäre Ressourcendatei im letzten Schritt des
Linkers mit den Objektdateien des C++-Quelltextes zur ausführbaren EXE-Datei
zusammengebunden.
Ressourcen-IDs
Zu jeder Ressourcendefinition gehört die Angabe einer Ressourcen-ID. IDs sollten
eindeutig sein, da mit ihrer Hilfe die einzelnen Ressourcen vom Programm aus
angesprochen werden.
Diese Ressourcen-IDs müssen dem C++-Compiler bekannt gemacht werden. Dazu
müssen die einzelnen Ressourcen-IDs mit Hilfe von #define-Direktiven mit IntegerKonstanten verbunden werden. Am sinnvollsten geschieht das mit einer eigenen
Header-Datei178, denn diese Header-Datei muß in die Ressourcenskriptdatei und in
die Quelldateien, die Ressourcen verwenden, über die #include-Direktive
aufgenommen werden.
Die Ressourcenmethoden
177
178
rc.exe
Der MFC-Assiistent nennt diese Header-Datei standardmäßig resource.h
350
Die Programmiersprache C++
Der letzte Schritt besteht darin, die Ressourcen zur Anzeige und zum Arbeiten mit
der Ressource zu laden. Dazu stehen verschieden API-Funktionen und MFCMethoden bereit.
Ressourcen anlegen
Der grundlegende Ablauf zum Anlegen und Bearbeiten von Ressourcen ist
1.
2.
3.
4.
Eine neue Ressource anlegen.
Ressource bearbeiten
Ressource im Programm verwenden
Ressourcen kompilieren
Die verschiedenen Ressourcen-Arten
Dialogfelder
Tastaturkürzel
Bitmaps
Sie können mit dem Grafik-Editor erstellt werden.
Mauszeiger
Symbole (Icons)
Symbole sind Bitmaps, die mit dem Grafik-Editor bearbeitet werden.
Symbolleisten
Stringtabellen
Hier werden einzelne Strings mit IDs in Verbindung gebracht.
Ressourcen-Editor
Mit dem Ressourcen-Editor können Dialogseiten gestaltet werden, Menüs erzeugt
werden und Symbolflächen, Icons gezeichnet werden. Der Ressource-Editor kann
aufgerufen werden, in dem im Arbeitsbereich (Workspace) die Symbolfläche
Ressource View aktiviert wird. Zum Erzeugen und anschließenden Gestalten einer
neuen Dialogseite geht man zur Ressource DIALOG und wählt mit der rechten
Maustaste den Befehl DIALOG EINFÜGEN aus. Der Ressource-Editor erzeugt
daraufhin einen neue Dialogseite mit den Windows-Steuerelementen. Durch Klick
auf ein bestimmtes Steuerelement in der Werkzeugleiste kann anschließend das
Steuerelement auf der Dialogseite positioniert werden.
Falls neue Menüpunkte entworfen werden sollen, geht man im Ressource Editor
zum Ressource Menü. Je nach Anwendungstyp stehen eine oder zwei Menüleisten
zur Verfügung. Mit einem Doppelklick auf die geeignete Ressource ruft man den
Menü-Editor auf. Auf der Menüleiste findet man ein leeres Menü in Form eines
leeren Käschchens. Mit der linken Maustaste kann man das leere Menü an die
351
Die Programmiersprache C++
richtige Stelle positionieren. Außerdem befinden sich auf der Menüleiste eine
Vielzahl weitere Menübefehle, die für Anwendungen genutzt werden können.
Analog zum Menü-Editor kann auch mit dem Toolbar-Editor gearbeitet werden. Falls
eigenene Schaltflächen entworfen werden sollen, ist der Toolbar-Editor aufzurufen.
Auf der Leiste des Toolbar befinden sich die gemalten Schaltflächen, die der
AppWizzard erzeugt hat. Die Symbolschaltflächen können übermalt werden.
Der Code-Assistent179 ist das Werkzeug das den mit Hilfe des Ressouce Editor
erstellten Steuerelementen die gewünschte Funktionalität verleiht.
6.4.2 Erstellen von Dialogen und Menüs aus Ressourcen
Dialoge und / oder Menüs sollen aus Ressourcen generiert und mit der
Anwendungslogik verknüpft werden. Eine Ressource ist eine textuelle Beschreibung
eines Dialogs oder eine Menüs.
1. Dialoge
Grundlagen
Die zugehörige Ressourcen-Datei kann manuell oder interaktiv (mit einem grafischen
Editor) erstellt werden.
Erzeugen eines Ressource-Scripts.
-
Projekt mit der rechten Maustaste auswählen
"Hinzufügen" und dann "Hinzufügen Ressource" auswählen
In der Ressourcen-Ansicht eines Projekts kann dann mit einen Klick mit der rechten Maustaste
über "Hinzufügen" die Resource für einen Dialog erstellt werden.
Mit "Neu" wird danach der Dialogeditor mit einem zunächst leeren Dialog eröffnet.
Neben dem Resssource-Script wird aud die Datei resource.h generiert. Diese
Datei enthält für alle Bezeichner eine Integer-Zahl, da alle Symbolnamen
(Dialognamen, Dialogfeldnamen, etc.) auf ganzzahligen Konstanten abgebildet
werden. Dies wird durch Präprozessor-Direktiven realisiert. Es gilt das folgende
Namensschema:
- IDC_NAME:
- IDD_NAME:
- ID_NAME:
- IDR_NAME:
'C' für Control-Namen
'D' für Dialog-Namen
ohne dritten Buchstaben für Menü-Namen
'R' für die komplette Menü-Ressource
Zur Überprüfung der Bezeichner gibt es zwei Möglichkeiten:
-
179
Öffnen der Datei resource.h als Textdatei
Anzeige der Symbolnamen zusammen mit ihren Werten in einem Dialog
-- Auswahl des Ressorcen-Scripts in einer Resourcen-Ansicht mit der rechten Maustaste
-- Auswahl des Menüpunkts "Resource Symbols ..."
-- In dem dann erscheinenden Dialog können Symbole gelöscht und verändert werden.
vgl. 6.4.1.2
352
Die Programmiersprache C++
Zum Datenaustausch zwischen Anwendung und Oberfläche dient als Basisklasse
die Klasse CDialog, d.h.: Eine eigene Klasse erbt von CDialog (zur Übernahme der
Grundfunktionalität). CDialog besitzt nämlich eine Tabelle zur Behandlung von
Ereignissen. Die wichtigsten unterstützenden Ereignisse sind:
-
-
OnInitDialog
- Aufruf: Beim Initialisieren des Dialogs
- Aufgabe: Eintragen von Anwendungsdaten in Dialogfelder
OnOK
- Aufruf: Falls der Dialog mit OK beendet wurde
- Aufgabe: Auslesen der Dialogfelder und Übernahme der Daten in die Anwendung.
Der Datenaustausch zwischen Anwendung und Dialog erfolgt folgendermaßen:
-
-
Die Anwendung schreibt Daten in die Attribute der Dialog-Klasse
Die Dialogklasse schreibt die Daten während des Ereignisses OnInitDialog in die
Steuerelemente des Dialogs (Zugriff mit evtl. automatischer Typkonvertierung):
SetDlgItemText: String-Wert im Steuerelement eintragen (Control)
SetDlgItemInt: Integer-Wert in Control eintragen
Falls der Dialog mit OK beendet wird, wird die Ereignis-Methode OnOK() aufgerufen.
Die Dialogklasse liest die Steuerelemente aus und speichert die Resultate in den eigenen
Attributen (Zugriffe wie beim Schreiben, Methodennamen fangen mit Get an).
Die Anwendung liest die Attribute aus.
Bsp.180: Es ist eine dialogbasierte Anwendung zu erstellen, die über eine Dialogbox
Datenaustausch ermöglicht.
Aufgabenstellung: Über das Menü (ShowDialog) soll eine Dialogbox aufgerufen werden
Die Dialogbox hat folgendes Aussehen:
180
pr64230
353
Die Programmiersprache C++
Mit dem vertikal angeordneten Schieberegler (der Klasse CSliderCtrl) kann ein Wert eingestellt
werden, der in dem daneben stehenden Eingabe-Textfeld angezeigt wird, z.B.:
Beim Betätigen der OK-Schaltfläche wird im Hauptfenster die Einstellung protokolliert:
Wird die Schaltfläche Abbrechen betätigt, dann nimmt das Hauptfenster folgende Gestalt an:
Lösungsschritte:
1. Erstellen der zur Anwendung bzw. zum Hauptfenster gehörigen Klassen einschl. der zum
Hauptfenster zugehörigen Menü-Ressource
Dekaration der Klasse CMeineAnwendung: MeineAnwendung.h
#include <afxwin.h>
#include "resource.h"
class CMeineAnwendung : public CWinApp
{
public:
354
Die Programmiersprache C++
virtual BOOL InitInstance();
};
Methoden der Klasse CMeineAnwendung: MeineAnwendung.cpp
#include "MeineAnwendung.h"
#include "Hauptfenster.h"
#include "MeinDialog.h"
static CMeineAnwendung meineAnwendung;
BOOL CMeineAnwendung::InitInstance()
{
m_pMainWnd = new CHauptfenster;
m_pMainWnd ->ShowWindow(m_nCmdShow);
return TRUE;
}
Deklaration der Klasse CHauptfenster: Hauptfenster.h
#include <afxwin.h>
class CHauptfenster : public CFrameWnd
{
private:
int
mWert;
public:
CHauptfenster();
afx_msg void ShowDialog();
DECLARE_MESSAGE_MAP()
};
Methoden der Klasse CHauptfenster: Hauptfenster.cpp
#include "Hauptfenster.h"
#include "MeinDialog.h"
BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd)
ON_COMMAND(ID_SHOWDIALOG,ShowDialog)
END_MESSAGE_MAP()
CHauptfenster::CHauptfenster()
{
Create(NULL,"Dialogkommunikation",
WS_OVERLAPPEDWINDOW,CRect(0,0,360,200),NULL,
MAKEINTRESOURCE(IDR_MENU1));
mWert
= 0;
}
void CHauptfenster::ShowDialog()
{
CClientDC dc(this);
CMeinDialog dlg(this); // Erzeuge Dialogobjekt (1. Schritt)
dlg.wert
= mWert;
// Initialisiere Dialogvariable
if (dlg.DoModal() == IDOK)
{
mWert
= dlg.wert;
CString s;
s.Format("Eingestellter Wert: %d", mWert);
dc.TextOut(10,10,s);
}
else {
dc.TextOut(10,10,"Abbrechen wurde gedrueckt");
}
}
355
Die Programmiersprache C++
Menüs sind in Visual C++-Anwendungen als Ressourcen definiert. Man kann sie daher im Editor von
Visual C++ über RESSOURCENANSICHT im Arbeitsbereich entwerfen.
2. Erstellen der zur Dialog-Box zugehörigen Ressource mit dem Ressourcen-Editor.
356
Die Programmiersprache C++
In der Dialog-Box wird ein CSliderCtrl zur Einstellung eines Datenwerts verwendet. Der
CSliderCtrl wird vertikal eingesetzt und sendet WM_VSCROLL-Nachrichten bei jeder
Positionsänderung. Die Nachrichtentabelle der Klasse CMeinDialog sieht damit so aus:
BEGIN_MESSAGE_MAP(CMeinDialog,CDialog)
ON_WM_VSCROLL()
END_MESSAGE_MAP()
Die zugehörige Methode ist OnVScroll. Die Dialogklasse CMeinDialog bearbeitet dieses Ereignis.
3. Erstellen der Dialog-Box zugehörigen Klasse CMeinDialog
Deklaration der Klasse CMeinDialog: MeinDialog.h
#include <afxwin.h>
#include "resource.h"
class CMeinDialog : public CDialog
{
public:
int
wert;
CMeinDialog(CWnd* pParentWnd);
virtual ~CMeinDialog();
virtual BOOL OnInitDialog();
virtual void OnOK();
afx_msg void OnVScroll(UINT nSBCode,UINT nPos,
CScrollBar* pScrollBar);
DECLARE_MESSAGE_MAP()
};
Methoden der Klasse CMeinDialog: MeinDialog.cpp
#include <afxcmn.h>
#include "MeinDialog.h"
357
Die Programmiersprache C++
BEGIN_MESSAGE_MAP(CMeinDialog,CDialog)
ON_WM_VSCROLL()
END_MESSAGE_MAP()
CMeinDialog::CMeinDialog(CWnd* pParentWnd)
: CDialog(IDD_DIALOG1,pParentWnd)
{
}
CMeinDialog::~CMeinDialog() { }
BOOL CMeinDialog::OnInitDialog()
{
SetDlgItemInt(IDC_WERT,wert);
CSliderCtrl* pSlider;
pSlider = (CSliderCtrl*) GetDlgItem(IDC_WERT_SLIDER);
pSlider->SetRange(0,99);
pSlider->SetPos(wert);
pSlider->SetTicFreq(5);
pSlider->SetPageSize(5);
return TRUE;
}
void CMeinDialog::OnOK()
{
wert = GetDlgItemInt(IDC_WERT);
CDialog::OnOK();
}
void CMeinDialog::OnVScroll(UINT nSBCode,UINT nPos,CScrollBar* pScrollBar)
{
// Wenn der Slider ausgerichtet ist,
// empfaengt diese Funktion die Nachricht
// Zugriff auf das Steuerelement
CSliderCtrl* pSlider;
pSlider = (CSliderCtrl*) GetDlgItem(IDC_WERT_SLIDER);
wert = pSlider->GetPos();
// Aendere den Wert
SetDlgItemInt(IDC_WERT,wert);
}
4. Arbeitsschritte nach dem Aktivieren des Menüpunkts ShowDialog
Nach dem Aktivieren des Menüpunkts ShowDialog werden folgende Schritte (vgl. void
CHauptfenster::ShowDialog() )durchlaufen:
1. CMeinDialog dlg(this); // Erzeuge Dialogobjekt (1. Schritt)
- Ein Objekt der zum Dialog gehörenden Klasse wird erzeugt
- this ist das Vaterobjekt
2. CDialog(IDD_RESSOURCE_ID,parent)
- Der Dialog-Konstruktor wird mit der ID der Ressource sowie dem Vaterobjekt aufgerufen
3. dlg.wert
= mWert; // Initialisiere Dialogvariable (2. Schritt)
- Die Attribute der Dialogklasse werden mit Startwerten belegt
4. dlg.DoModal()
- Der Dialog wird modal abgefragt
5. OnInitDialog()
- DoModal() ruft diese Initialisierungsmethode der Dialogklasse auf
- Dort wird mit SetDlgItemXXX der Startwert aus den Attributen in die Controls eingetragen
6. OnOK()
- Aufruf dieser Methode, wenn der Benutzer den Dialog mit OK abgeschlossen hat.
- Dort wird mit GetDlgItemXXX der Wert des Controls ausgelesen und in den Attributen
gespeichert
- Bei Auswahl des Abbruch-Buttons wird die Methode OnCancel aufgerufen.
7. if (dlg.DoModal() == IDOK)
- Test, ob der Dialog mit OK abgeschlossen wurde
8. mWert = dlg.wert
358
Die Programmiersprache C++
- die gefüllten Attribute aus dem Objekt der Dialogklasse auslesen und in eigenen Attributwerten
speichern.
Reduktion des Datenautauschs bei Dialogen auf das Lesen und Schreiben von
Variablen
Es gibt ein Verfahren, das den Austausch auf das Lesen und Schreiben von
Variablen reduziert. Zusätzlich können automatisch einfache Eingabeprüfungen
vorgenommen werden. Der Austausch kann komplett interaktiv generiert werden.
Funktionsweise im modalen Dialog
- OnInitDialog ruft UpdateData(false) auf (-> Beschreiben des Dialogs)
- Arbeit mit dem Dialog, bis OK oder Abbruch gewählt wurde
- Vor Beendigung der Methode DoModal() ruft das Framework OnOK() auf, das seinerseits
UpdateData(true) aufruft. (-> Auslesen des Dialogs)
Funktionsweise im nicht-modalen Dialog
- Die Anwendung oder OnInitDialog rufen UpdateData(false) auf (-> Beschreiben des Dialogs).
- Arbeit mit dem Dialog
- Die Anwendung ruft UpdateData(true) auf (-> Auslesen des Dialogs).
UpdateData ruft die virtuelle Methode DoDataExchange auf.
- Diese Methode muß implementiert werden
- Die Methode kopiert die Daten (für beide Richtungen)
- DoDataExchange kann die Gültigkeit der Daten prüfen und automatisch Fehlermeldungen
ausgeben
Bsp181. für den Aufbau der Methode DoDataExchange
void CMeinDialog::DoDataExchange(CDataExchange* pDx)
{
CDialog::DoDataExchange(pDx);
DDX_Text(pDx,IDC_MITTEILUNG,mitteilung);
DDV_MaxChars(pDx,mitteilung,30); // Max. 30 Zeichen
DDX_Text(pDx,IDC_TEMP,temp);
DDV_MinMaxInt(pDx,temp,0,50);
// Temperatur zwischen 0 und 50
}
Die Funktionen DDX_???? kopieren Daten Die Funktionen DDV_???? überprüfen
Daten.
Datenaustausch. Für den Zugriff auf Controls existieren über 50 verschiedene
(überladene Funktionen), z.B.:
- DDX_Text
- liest textuelle Inhalte aus Controls und trägt sie in verschiedene Variablentypen ein
- der Zieldatentyp kann u.a. byte, short, int, UINT, long, DWORD, Cstring, float und double sein
- der Text wird automatisch konvertiert
- DDX_Check
- greift auf den Wert einer Checkbox zu
- erlaubte Werte sind: BST_CHECKED, BST_INDETERMINATE und BST_UNCHECKED
- DDX_DataTimeCtrl
- greift auf den Wert des Controls CDataTimeCtrl zu
181
pr64220
359
Die Programmiersprache C++
Datenüberprüfung. Im Fehlerfall wird automatisch ein Dialog angezeigt, und der
Dialog nicht verlassen. Es gibt 12 verschiedene Funktionen zur Überprüfung der
Eingabe, z.B.:
- DDV_MinMaxInt
Der Integer-Wert muß in einem bestimmten angegebenen Zahlenbereich liegen
- DDV_MaxChars
Die Eingabe im Feld darf eine Anzahl Zeichen nicht überschreiten
- DDV_MinMaxDataTime
Datum und Uhrzeit müssen in einem bestimmten Bereich liegen
Bsp.182: Erstelle eine dialogbasierte Anwendung, die über eine Dialogbox
Datenaustausch über DoDataExchange() ermöglicht
Aufgabenstellung.
Über das Menü (ShowDialog) des Hauptfensters soll eine Dialogbox aufgerufen werden.
Die Dialogbox hat folgendes Aussehen:
In das obere Editierfeld kann eine Mitteilung (z.B.: "Temperaturwert (Grad Celsius)") eingeschrieben
werden, die Auskunft über den im zweiten Editierfeld angegebenen Wert gibt. Die Wertangabe darf nur
Zahlenwerte von 0 bis 50 umfassen, der im ersten Editierfeld angegebene Text darf nicht mehr als 30
Zeichen umfassen. Wird der Zahlenbereich bei der Wertangabe verletzt, z.B. durch folgenden Eintrag
in der Dialogbox
182
pr64220
360
Die Programmiersprache C++
, dann öffnet eine MessageBox folgendes Fenster:
Mit einem Klick auf OK wird wieser zurück ins Hauptfenster verzweigt. Ist die Eingabe in der Dialogbox
korrekt, dann erscheint nach Klick auf OK in der Dialogbox bspw. folgendes Hauptfenster:
Wird in der Dialogbox Abbrechen gedrückt, dann wird folgende Mitteilung im Hauptfenster
ausgegeben:
Lösungsschritte
1. Erstellen der zur Anwendung bzw. zum Hauptfenster gehörigen Klassen einschl. der zum
Hauptfenster zugehörigen Menü-Ressource. Es gibt keine wesentlichen Veränderungen zum letzten
Beispiel.
361
Die Programmiersprache C++
2. Erstellen der zur Dialog-Box zugehörigen Ressource mit dem Ressourcen-Editor. Es gibt keine
wesentlichen Veränderungen zum letzten Beispiel.
3. Erstellen der Dialog-Box zugehörigen Klasse CMeinDialog
Deklaration der Klasse CMeinDialog: MeinDialog.h
#include <afxwin.h>
#include "resource.h"
class CMeinDialog : public CDialog
{
public:
CString mitteilung; // fuer IDC_MITTEILUNG
int
temp;
// fuer IDC_TEMP
CMeinDialog(CWnd* pParentWnd);
virtual ~CMeinDialog();
virtual void DoDataExchange(CDataExchange *pDx);
};
Methoden der Klasse CMeinDialog: MeinDialog.cpp
#include "MeinDialog.h"
CMeinDialog::CMeinDialog(CWnd* pParentWnd)
: CDialog(IDD_DIALOG1,pParentWnd)
{
mitteilung = _T("");
temp = 0;
}
CMeinDialog::~CMeinDialog() { }
void CMeinDialog::DoDataExchange(CDataExchange* pDx)
{
CDialog::DoDataExchange(pDx);
DDX_Text(pDx,IDC_MITTEILUNG,mitteilung);
DDV_MaxChars(pDx,mitteilung,30); // Max. 30 Zeichen
DDX_Text(pDx,IDC_TEMP,temp);
DDV_MinMaxInt(pDx,temp,0,50);
// Temperatur zwischen 0 und 50
}
4. Hinweis: Die Ausgabe der MessageBox erfordert folgende Einträge in der " .rc"-Datei:
3 TEXTINCLUDE
BEGIN
"\r\n"
"#include ""l.deu\afxres.rc"""
"\0"
END
und
#ifndef APSTUDIO_INVOKED
//
#include "l.deu\afxres.rc"
//
#endif
// not APSTUDIO_INVOKED
362
Die Programmiersprache C++
2. Menüs
Grundlagen
Auch Menüs werden mit Hilfe des Resourcen Editor erstellt. Es gibt zwei
Möglichkeiten ein Menü zu einem Fenster hinzuzufügen:
1. Ein Fenster wird durch die Klasse CFrameWnd implementiert. Im Aufruf des Fensters enthält
der
6.
Parameter
eine
Angabe
zum
Menü:
Create(...,MAKEINTRESOURCE(IDR_MENU));. MAKEINTRESOURCE ist ein Makro, das
eine Resource-ID in einen Resource-Typ für die Win32-API umwandelt.
2. Der 6. Parameter der Create-Methode ist NULL. Mit folgenden Schritten kann dann das Menü
manuell erstellt werden:
CMenu menu;
menu.LoadMenu(IDR_MENU);
SetMenu(&menu);
menu.Detach();
Menüs werden mit Hilfe des Ressourcen-Editor erstellt, z.B.
Intern wird eine Ressource-Datei erstellt, die das Menü bearbeitet:
IDR_MENU1 MENU
BEGIN
POPUP "OptionA"
BEGIN
MENUITEM "A-1",
MENUITEM "A-2",
END
POPUP "OptionB"
BEGIN
MENUITEM "B-1",
ID_OPTIONA_A1
ID_OPTIONA_A2
ID_OPTIONB_B1
363
Die Programmiersprache C++
MENUITEM "B-2",
MENUITEM "B-3",
ID_OPTIONB_B2
ID_OPTIONB_B3
END
MENUITEM "Clear",
ID_CLEAR
END
Auch für Menüs werden Ereignis-Tabellen (Message-Maps) verwendet, um
Ereignissen Methoden zuzuordnen:
-
Einfache
Zuordnung
eines
Ereignisses
zu
einer
Methode,
z.B.
ON_COMMAND(ID_OPTIONA_A1,DoOptionA1)
-- Bei Auswahl des Menüpunkts mit der ID ID_Option_A1 wird der Methode void DoOptionA1()
aufgerufen.
- Zuordnung eines Bereichs von Menü-IDs zu einer methode für eine Auswahl von Menüs, z.B.
ON_COMMAND_RANGE(ID_OPTIONB_B1,ID_OPTIONB_B3,OnOptionB)
-- Die Menü-IDs müssen aufeinanderfolgende IDs besitzen. (Dies muß evtl. durch manuellen Eingriff
sichergestellt werden).
-- Bei Auswahl einer der Menüpunkte aus dem Bereich wird die Methode void DoOptionB(UINT nID)
aufgerufen.
Implementierung: Das erzeugte Menü soll zu der folgenden Anwendung erweitert
werden:
Wird bspw. OptionA aktiviert und Menüpunkt A2 aufgerufen, dann erscheint im Hautfenster ein Text.
Zur Implementierung sind folgende Dateien aufzunehmen:
MeineAnwendung.h
#include <afxwin.h>
class CMeineAnwendung : public CWinApp
{
public:
virtual BOOL InitInstance();
};
MeineAnwendung.cpp
#include "MeineAnwendung.h"
#include "Hauptfenster.h"
static CMeineAnwendung meineAnwendung;
BOOL CMeineAnwendung::InitInstance()
{
m_pMainWnd = new CHauptfenster;
m_pMainWnd ->ShowWindow(m_nCmdShow);
return TRUE;
364
Die Programmiersprache C++
}
Hauptfenster.h
#include <afxwin.h>
class CHauptfenster : public CFrameWnd
{
private:
char* text;
public:
CHauptfenster();
afx_msg void OnPaint();
afx_msg void DoOptionA1();
afx_msg void DoOptionA2();
afx_msg void DoOptionB(UINT nID);
afx_msg void DoClear();
DECLARE_MESSAGE_MAP()
};
Hauptfenster.cpp
#include "Hauptfenster.h"
#include "resource.h"
BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd)
ON_WM_PAINT()
ON_COMMAND(ID_OPTIONA_A1,DoOptionA1)
ON_COMMAND(ID_OPTIONA_A2,DoOptionA2)
ON_COMMAND_RANGE(ID_OPTIONB_B1,ID_OPTIONB_B3,DoOptionB)
ON_COMMAND(ID_CLEAR,DoClear)
END_MESSAGE_MAP()
CHauptfenster::CHauptfenster()
{
Create(NULL,"Erstes Menue",
WS_OVERLAPPEDWINDOW,CRect(0,0,280,200),NULL,
MAKEINTRESOURCE(IDR_MENU1));
text = NULL;
}
void CHauptfenster::DoOptionA1()
{
text = "Option A1";
Invalidate();
}
void CHauptfenster::DoOptionA2()
{
text = "Option A2";
Invalidate();
}
void CHauptfenster::DoOptionB(UINT nID)
{
switch(nID)
{
case ID_OPTIONB_B1:
text = "Option B1"; break;
case ID_OPTIONB_B2:
text = "Option B2"; break;
case ID_OPTIONB_B3:
text = "Option B3"; break;
}
Invalidate();
}
void CHauptfenster::DoClear()
{
text = NULL;
Invalidate();
365
Die Programmiersprache C++
}
void CHauptfenster::OnPaint()
{
if (text != NULL)
{
CPaintDC dc(this);
dc.TextOut(10,10,text);
}
}
Zustandänderungen
Menüs können Zustände darstellen. Es gibt zwei Ansätze dafür: Ein alter und neuer
Ansatz.
Ziel des neuen Ansatzes ist die Trennung der Darstellung durch 2 getrennte
Ereignisse:
1. Das erste Ereignis wird ausgelöst, kurz bevor das Menü angezeigt werden soll. Hier wird der
Zustand in das Menü eingetragen.
2. Das zweite Ereignis nach Auswahl des Menüs wird wie bisher ausgelöst.
Vorgehensweise.
- Der Entwickler verwaltet die Zustände der Menüpunkte selbst im Programm.
- Trtt das neue zusätzliche Ereignis ON_WM_INITMENUPOPUP ein, dann ruft MFC eine Reihe von
Methoden auf, falls diese in der Ereignistabelle eingetragen sind.
- Diese Methoden tragen den jeweiligen Zustand in den Menüeintrag ein.
Beispieleintrag
in
der
Ereignistabelle:
ON_UPDATE_COMMAND_UI(ID_OPTIONA_A1,
DoUpdateOptionA2)
Dadurch
wird
eine
Methode
mit
folgender
Signatur
aufgerufen:
void
DoUpdateOptionA2(CCmdUI* pCmdUI);
- Der Parameter CCmdUI ist ein Zeiger auf den Eintrag, der das Ereignis ausgelöst hat.
- Der Eintrag kann ein Menü oder eine ähnliche Komponente sein (z.B. Toolbar-Button).
- CCmdUI hat folgende Methoden zur Änderung des Zustands
Enable(BOOL bFlag): Eintrag aktivieren oder deaktivieren
SetCheck(UINT nState):BST_UNCHECKED(0), BST_CHECKED(1), BST_INTERMINATE(2)
setRadion(BOOL): Setzt oder löscht die Marke.
setText(char *): Ändert den Text des Eintrags.
- Sollen mehrere Einträge mit benachbarten IDs gemeinsam behandelt werden, so können diese mit
einem Ereignis bearbeitet werden: ON_UPDATE_COMMAND_UI_RANGE(ID_RANGE_START,
ID_RANGE_END, DoUpdateOptionRange)
366
Die Programmiersprache C++
6.4.3 Visuelle Erstellung
Dialogfeldbasierende Anwendung
1. Erstelle ein neues MFC-Applikations-Visual-C++-Projekt
2. Erstelle den Anwendungsrahmen mit dem Anwendungs-Assistenten in folgenden
Schritten:
1. Festelegen des Anwendungstyps "Auf Dialogfeldern basierend"
Abb.: Festlegen des Anwendungstyps
3. Unter BENUTZEROBERFLÄCHENFEATURES (Mekmale für die Benutzerschnittstelle)
Aussehen des Hauptfensters für die Anwendung festlegen: Ändern des Eintrags im
Dialogfeldtitel in "1. MFC-Assistenten-Anwendung".
367
Die Programmiersprache C++
Abb.
3. Klick auf FERTIG STELLEN, damit der MFC-Anwendungs-Assistent den Anwendungsrahmen
erstellen kann.
4. Der Arbeitsbereich zeigt nun:
368
Die Programmiersprache C++
Abb.
Im Fensterbereich erscheint eine Standard-Dialog-Vorlage mit einem Textfeld, einem OK- und
einem Abbrechen-Button, die verändert / ergänzt werden können. Hierzu steht eine
Werkzeugkasten Toolbox mit Steuerelementen bereit (auf der linken Seite).
3. Was hat der Assistent erzeugt?
Mit dem Windows-Explorer lassen sich über das Projektmappenverzeichnis folgende
Dateien feststellen:
369
Die Programmiersprache C++
Abb.
1. In der Datei ReadMe.txt befindet sich u. a. in Auszügen
pr64310.vcproj
Dies ist die Hauptprojektdatei für VC++-Projekte, die vom Anwendungs-Assistenten
erstellt wird. Sie enthält Informationen über die Version von Visual C++, mit der
die Datei generiert wurde, über die Plattformen, Konfigurationen und Projektfeatures,
die mit dem Anwendungs-Assistenten ausgewählt wurden.
pr64310.h
Hierbei handelt es sich um die Haupt-Headerdatei der Anwendung. Diese enthält
andere projektspezifische Header (einschließlich Resource.h) und deklariert die
Cpr64310App-Anwendungsklasse.
pr64310.cpp
Hierbei handelt es sich um die Haupt-Quellcodedatei der Anwendung. Diese enthält die
Anwendungsklasse Cpr64310App.
pr64310.rc
Hierbei handelt es sich um eine Auflistung aller Ressourcen von Microsoft Windows, die
vom Programm verwendet werden. Sie enthält die Symbole, Bitmaps und Cursors, die im
Unterverzeichnis RES gespeichert sind. Diese Datei lässt sich direkt in Microsoft
Visual C++ bearbeiten.
res\pr64310.ico
Dies ist eine Symboldatei, die als Symbol für die Anwendung verwendet wird. Dieses
Symbol wird durch die Haupt-Ressourcendatei pr64310.rc eingebunden.
res\pr64310.rc2
Diese Datei enthält Ressourcen, die nicht von Microsoft Visual C++ bearbeitet wurden.
In dieser Datei werden alle Ressourcen gespeichert, die vom Ressourcen-Editor nicht bearbeitet
werden können.
370
Die Programmiersprache C++
Der Anwendungs-Assistent erstellt eine Dialogklasse: pr64310Dlg.h, pr64310Dlg.cpp - das Dialogfeld
Diese Dateien enthalten die Klasse Cpr64310Dlg. Diese Klasse legt das
Verhalten des Haupt-Dialogfelds der Anwendung fest. Die Vorlage des Dialogfelds befindet sich in pr64310.rc, die mit Microsoft Visual C++ bearbeitet werden kann.
StdAfx.h, StdAfx.cpp
Mit diesen Dateien werden vorkompilierte Headerdateien (PCH) mit der Bezeichnung
pr64310.pch und eine vorkompilierte Typdatei mit der Bezeichnung StdAfx.obj erstellt.
Resource.h
Dies ist die Standard-Headerdatei, die neue Ressourcen-IDs definiert.
Microsoft Visual C++ liest und aktualisiert diese Datei.
2. Die folgenden Dateien sind in ReadMe.txt nicht ausgeführt:
pr64310.aps
Binäre Version der aktuellen Ressourcenskriptdatei.
pr64310.ncb
Zugang mit dem Texteditor nur bei geschlossenem Arbeitsbereich möglich.
(Absolutes Sperrgebiet, da hier ständig Details vom Projekt dokumentiert werden).
pr64310.sln
3. Im Unterverzeichnis ...\res findet man:
pr64310.ico
32*32 Pixel-Icon mit MFC-Darstellung
pr64310.rc2
Öffnet man diese Datei mit einem Texteditor, dann liest man folgendes:
//
// pr64310.RC2 - Ressourcen, die Microsoft Visual C++ nicht direkt bearbeitet
//
#ifdef APSTUDIO_INVOKED
#error this file is not editable by Microsoft Visual C++
#endif //APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
// Fügen Sie hier manuell bearbeitete Ressourcen hinzu...
/////////////////////////////////////////////////////////////////////////////
4. Ressourcen-Ansicht pr64310
IDD_PR64310_DIALOG
ist der Name des Dialogfelds. Durch Doppelklick darauf gelangt man im Fensterbereich zu der
Ressource, die man dort auch bearbeiten kann.
IDR_MAINFRAME
zeigt auf ein 32*32 Pixel-Icon, das die Buchstaben MFC grafisch verarbeitet.
VS_VERSION_INFO
beinhaltet Versions-Informationen, die vom Assistenten erzeugt wurden.
371
Die Programmiersprache C++
Abb.
4. Kompilieren und Linken
Durch Kompilieren und Linken ist nun pr64310.exe entstanden. Nach dem Start
zeigt sich das folgende Bild:
Abb.: Das Fenster nach dem ersten Testlauf
372
Die Programmiersprache C++
Nach dem Start wird das Dialogfenster in der Bildschirmmitte platziert. Durch Festhalten der Titelleiste
mit der linkten Maustaste kann man das Fenster beliebig verschieben (ziehen). Die Anwendung
schließt direkt durch Anklicken des OK-Buttons, des Abbrechen-Buttons und des "X" rechts oben.
Durch Anklicken des Icon links oben öffnet sich ein Popup-Menü mit zwei Menü-Punkten. Das Fenster
kann nicht in der Größe verändert, nach unten geglickt (minimiert) oder auf volle Größe (maximiert)
gebracht werden. Ein Rand Ist vorhanden, man kann ihn jedoch nicht mit der Maus zur Veränderung
der Größe ziehen.
5: Untersuchen der Arbeitsumgebung nach dem Titel der Dialoganwendung
Öffnen (ohne zu speichern) mit einem Texteditor (z.B. Notizblock, Wordpad aus dem WindowsZubehör) die Datei pr64310.rc. Im Abschnitt "Dialog" finden sich folgenden Eintragungen:
IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200
STYLE DS_SHELLFONT | WS_POPUP | WS_VISIBLE | WS_CAPTION
| DS_MODALFRAME
| WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "1. MFC-Assistenten-Anwendung"
FONT 8, "MS Shell Dlg"
BEGIN
DEFPUSHBUTTON "OK",IDOK,263,7,50,16
PUSHBUTTON
"Abbrechen",IDCANCEL,263,25,50,16
CTEXT
"TODO: Dialogfeld-Steuerelemente hier positionieren.",IDC_STATIC,10,96,300,8
END
In der ersten Zeile IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200 findet sich der Name IDD_PR64310_DIALOG
(nameID), der der Dialog-Ressource zugeordnet wurde. Die Ausdehnung ist 320*200 (Breite und Höhe
in Pixel). Die nameID ist in der Datei resource.h definiert:
#define IDR_MAINFRAME
#define IDM_ABOUTBOX
#define IDD_ABOUTBOX
#define IDS_ABOUTBOX
#define IDD_PR64310_DIALOG
128
0x0010
100
101
102
Windows verwendet zur Identifizierung die Dialogressource den Wert 102. IDR_MAINFRAME ist der
Name für das MFC-Menü.
Im Ressourcenskript findet man danach:
STYLE DS_SHELLFONT | WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | WS_SYSMENU
Hier werden die verschiedenen Window Styles (WS) abgelegt. WS_CAPTION bedeutet, dass das
Fenster eine Titelzeile hat. WS_SYSMENU liefert das Menü links oben und das "X" rechts oben, mit
dem man die Anwendung einfach beenden kann. Die senkrechten Striche sind die bitweise ODERVerknüpfungen der verschiedenen Window Styles.
EXSTYLE WS_EX_APPWINDOW
ist die erweiterte Version von STYLE und definiert WS_EX_APP_WINDOW.
CAPTION "1. MFC-Assistenten-Anwendung"
legt die Titelzeile des Fensters fest.
FONT 8, "MS Shell Dlg"
Die zu verwendende Schrift wird hier vergeben.
BEGIN
DEFPUSHBUTTON "OK",IDOK,263,7,50,16
PUSHBUTTON
"Abbrechen",IDCANCEL,263,25,50,16
CTEXT
"TODO: Dialogfeld-Steuerelemente hier positionieren.",IDC_STATIC,10,96,300,8
END
Zwischen BEGIN und END steht
- Button mit der Aufschrift "OK" mit den Dialogfeldkoordinaten 263,7,50,16 und der Kennung IDOK
- Button mit der Aufschrift "Abbrechen" mit den Dilogfeldkoordinaten 263,25,50,16 und Kennung
IDCANCEL
373
Die Programmiersprache C++
- Linksbündiges Textfeld. IDC_STATIC definiert ein statisches Textfeld.
6. Die diversen Dateien für Ressourcen
Mit dem grafischen Editor wird eine Ressourcenscriptdatei (.rc) und die Datei resource.h erzeugt. Der
Explorer zeigt im Projektmappen-Unterverzeichnis Debug die Datei pr64310.res, eine binäre Datei,
die vom Ressourcen-Compiler aus der Ressourcen-Dateiscriptdatei (.rc) erzeugt wurde.
7. Experimente
1. Verändern des Dialogfensters mit dem grafischen Ressourcen-Editor
-
-
Doppelklick auf IDD_PR64310_DIALOG. Im daraufhin erscheinenden Fenster Eigenschaften unter
Darstellung | Beschriftung "Neuer Titel" angeben.
Entfernen OK- und Abbrechen-Taste (durch Anklicken des jeweiligen Steuerelements und
Drücken der Taste "Entf").
Ändern des Textfelds (rechte Maustaste drücken, unter Eigenschaften, Text-Eigenschaften |
Formate bei "Text ausrichten" eingeben: zentriert(e) (Darstellung). Die Veränderung des
Textfeldinhalts erfolgt unter "Text-Eigenschaften" im Eingabefeld Beschriftung ("Hallo Welt!!!").
Nach dem Übersetzen, Linken, Starten erscheint jetzt folgendes Fenster:
pr64310.rc sieht jetzt so aus:
IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE |
WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "Neuer Titel"
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
CTEXT
"Hallo Welt!!!",IDC_STATIC,10,96,300,8
END
2. "X" oben rechts soll jetzt verschwinden.
Das erfolgt über den Eintrag "False" im Systemmenü unter Eigenschaften | Darstellung.
Die Datei pr64310.rc sieht jetzt so aus:
IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE |
WS_CAPTION
EXSTYLE WS_EX_APPWINDOW
374
Die Programmiersprache C++
CAPTION "Neuer Titel"
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
CTEXT
"Hallo Welt!!!",IDC_STATIC,10,96,300,8
END
Das Fenster kann jetzt nur noch mit "Alt + F4" geschlossen werden.
3. Entfernen von WS_CAPTION
Mit "Alt+F4" kann das Fenster geschlossen werden.
4. WS_VISIBLE durch Verändern des Eintrags zu IDD_PR64310_DIALOG in Eigenschaften |
Verhalten unter Sichtbar von True nach False deaktivieren.
Nach Übersetzen, Linken, Starten ergibt sich keine Veränderung. Das Fenster bleibt sichtbar.
Die Datei pr64310.rc sieht jetzt so aus:
IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 201
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP
EXSTYLE WS_EX_APPWINDOW
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
CTEXT
"Hallo Welt!!!",IDC_STATIC,10,96,300,8
END
WS_VISIBLE legt fest, ob ein bestimmtes Fenster von Anfang an sichtbar ist oder nicht. Ist dies nicht
der Fall, muß man das Fenster im Programm mit ShowWindow() bzw. bei Dialogen mit DoModal()
explizit sichtbar machen. Da es sich hier um das Hauptfenster handelt, erledigt dies der Assistent.
BOOL Cpr64310App::InitInstance()
{
....
Cpr64310Dlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
....
....
};
5. Entferne aus dem Ressourcenscript alle Einträge in der mit STYLE beginnenden Zeile bis auf
WS_POPUP mit einem Texteditor.
375
Die Programmiersprache C++
IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 201
STYLE WS_POPUP
EXSTYLE WS_EX_APPWINDOW
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
CTEXT
"Hallo Welt!!!",IDC_STATIC,10,96,300,8
END
Beim Abspeichern erscheint folgende Meldung:
Es wird offensichtlich sorgfältig über die Dateien des Projekts gewacht. Da die manuellen Änderungen
übernommen werden sollen, wird mit "Ja" geantwortet.
Nach Starten, Linken, Ausführen erscheint das folgende Fenster:
Auch das Entfernen von "Hallo Welt!!!" und das Herausstreichen von
STYLE WS_POPUP
EXSTYLE WS_EX_APPWINDOW
mit einem Texteditor führt noch zur Ausgabe eines Fensters (graue Fläche) mit den angegebenen
Abmessungen. pr64310.rc kann bis auf
IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 201
BEGIN
END
abgemagert werden. Es erscheint immer noch ein Fenster nach Starten, Linken, Ausführen, das mit
"Alt + F4" geschlossen werden kann.
6. Nachdem das Dialog-Fenster auf eine ganze Fläche ohne Rand reduziert ist, öffne pr64310.rc mit
Hilfe des Texteditors und führe folgende Veränderungen aus:
376
Die Programmiersprache C++
IDD_PR64310_DIALOG DIALOGEX 0, 0, 150, 150
STYLE WS_POPUP
FONT 10,"MS Sans Serif"
BEGIN
END
Die Angabe STYLE WS_POPUP ist hier nötig, da sonst automatisch der Kombi-Typ
WS_POPUPWINDOW (= WS_POPUP | WS_BORDER | WS_SYSMENU) erzeugt wird. Das
Systemmenü wird nur dann nicht angezeigt, solange WS_CAPTION noch fehlt.WS_BORDER ergibt
einen dünnen Rand. Werden BEGIN END weggelassen, gibt es zahlreiche Fehlermeldungen.
Nach Starten, Linken, Ausführen erscheint jetzt eine quadratische (graue Fensterfläche) ohne weitere
Dekoration.
7. Neuaufbau des Dialogs über die Resourcenansicht, Klick auf IDD_PR64310_DIALOG, verändern
über EIGENSCHAFTEN Stil in "Überlappend" und Rahmen in "Dialogfeldrahmen" bzw. aktivieren
Systemmenü, Minimieren-, Maximieren-Schaltfläche.
pr64310.rc besitzt danach folgende Gestalt:
IDD_PR64310_DIALOG DIALOGEX 0, 0, 150, 157
STYLE DS_SETFONT | DS_MODALFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX |
WS_CAPTION | WS_SYSMENU
FONT 10, "MS Sans Serif", 0, 0, 0x1
BEGIN
END
Jetzt wird noch in Eigenschaften Beschriftung in "Neuaufgebauter eigener Dialog" und Clientkante von
"False" nach "True" geändert. Die Datei pr64310.rc hat folgende Gestalt angenommen:
IDD_PR64310_DIALOG DIALOGEX 0, 0, 150, 160
STYLE DS_SETFONT | DS_MODALFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX |
WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_CLIENTEDGE
CAPTION "Neuaufgebauter eigener Dialog"
FONT 10, "MS Sans Serif", 0, 0, 0x1
BEGIN
END
Das zugehörige Fenster hat folgende Gestalt:
377
Die Programmiersprache C++
6.4.4 Interaktive Platzierung der Komponenten
1. Ein einfaches Beispiel
Aufgabenstellung. Zwei Zahlen in Eingabetextfeldern, die miteinander multipliziert
und in ein Ausgabetextfeld gebracht werden sollen. Die Anwendung soll folgendes
Fenster zur Ein- bzw. Ausgabe der Zahlen benutzen.
Abb.:
Lösung mit Hilfe des Anwendungsassistenten über interaktive Platzierung der
Komponenten.
1. Erstelle ein neues MFC-Applikations-Visual-C++-Projekt
2. Erstelle den Anwendungsrahmen mit dem Anwendungs-Assistenten in folgenden
Schritten:
1. Festlegen des Anwendungstyps "Auf Dialogfeldern basierend"
2. Unter BENUTZEROBERFLÄCHENFEATURES (Mekmale für die Benutzerschnittstelle)
Aussehen des Hauptfensters für die Anwendung festlegen: "Ändern des Eintrags im
Dialogfeldtitel in "2. Anwendungsassistenten-Anwendung".
3. Klick auf FERTIG STELLEN, damit der MFC-Anwendungs-Assistent den Anwendungsrahmen
erstellen kann.
4. Der Arbeitsbereich zeigt nun:
378
Die Programmiersprache C++
Abb.: Arbeitsbereich mit einer Baumansicht der Projektklassen
5. Kompilieren der Anwendung über ERSTELLEN | PROJEKTMAPPE erstellen.
6. Die KLassenansicht zeigt drei Klassen:
Abb.: Klassenansicht
7. Start der Anwendung mit START | STARTEN OHNE DEBUGGEN
379
Die Programmiersprache C++
Abb.: Ausgeführte Anwendung ohne Änderung des Projektgerüsts
Wird auf das Icon (links oben in der Titelleiste geglickt) erscheint das System-Menü, das auch zur
"AboutBox" führt
Die Wahl des Menüpunkts, der zum Aufruf dieses Info-Felds führt, erzeugt eine Nachricht, die
folgende Member-Funktion startet:
void Cpr64410Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialog::OnSysCommand(nID, lParam);
}
}
Hier wird das Objekt dlgAbout der von der MFC-Fenster-Klasse CDialog abgeleiteten Klasse
CAboutDlg deklariert.
Aus dem Ressourcen-Skript erhält man die Beschreibung der Ressourcen für das Dialogfeld:
IDD_ABOUTBOX DIALOGEX 0, 0, 235, 55
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION |
WS_SYSMENU
CAPTION "Info über pr64410"
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
380
Die Programmiersprache C++
ICON
LTEXT
LTEXT
DEFPUSHBUTTON
IDR_MAINFRAME,IDC_STATIC,11,17,20,20
"pr64410 Version 1.0",
IDC_STATIC,40,10,119,8,SS_NOPREFIX
"Copyright (C) 2003",IDC_STATIC,40,25,119,8
"OK",IDOK,178,7,50,16,WS_GROUP
END
3. Gestalten des Hauptdialogfelds nach folgender Vorlage:
4. Die Konfiguration der Eigenschaften der Steuerelemente zeigt der folgende
Ausschnitt aus dem Ressourcen-Skript:
IDD_PR64410_DIALOG DIALOGEX 0, 0, 320, 117
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE |
WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "2. Anwendungsassisten-Anwendung"
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
DEFPUSHBUTTON
"OK",IDOK,263,7,50,16
EDITTEXT
IDC_EDIT1,112,19,77,15,ES_AUTOHSCROLL
LTEXT
"Eingabe 1",IDC_STATIC,31,20,61,11
EDITTEXT
IDC_EDIT2,111,41,77,14,ES_AUTOHSCROLL
LTEXT
"Eingabe 2",IDC_STATIC,32,42,60,13
PUSHBUTTON
"Ausgabe",IDC_BUTTON1,266,77,47,15
EDITTEXT
IDC_EDIT4,113,79,76,15,ES_AUTOHSCROLL | ES_READONLY
LTEXT
"Ausgabe",IDC_STATIC,32,82,49,13
PUSHBUTTON
"Abbrechen",IDCANCEL,263,25,50,16
END
5. Festlegen der Tabulator-Reihenfolge von Steuerelementen
Dadurch wird sichergestellt, dass der Benutzer bei der Navigation mit Hilfe der TabTaste die Steuerelemnte in der gewünschten Reihenfolge ansprcht
1. Markiere im Berabeitungsbereich von Visual Studio entweder das Dialogfeld oder eines der
Steuerelemente im Fenster
2. Wahl von FORMAT / TABULATOR_REIHENFOLGE.
Daraufhin erscheinen im Fenster neben den Steuerelementen Nummern. Diese kennzeichnen die
Reihenfolge, in der die Navigation durch das Dialogfeld verläuft. Verändern der Reihenfolge durch
Anklicken der Nummernfelder in der Reihenfolge, in der der Benutzer durch das Dialogfeld navigieren
soll. Die Steuerelemente nummerieren sich automatisch neu.
381
Die Programmiersprache C++
6. Verbinden von Variablen mit Steuerelementen, z.B.
382
Die Programmiersprache C++
6. Ausstatten der Steuerelemente mit Funktionalität
Rechtsklick auf die Schaltfläche "Ausgabe" im Ressourcen-Editor für das Dialogfeld. Darauf erscheint
ein Kontextmenü. Dort erfolgt die Auswahl des Menüpunkts Eigenschaften. Unter Eigenschaften wird
"der glebe Blitz" gewählt.
Der Eintrag BN_CLICKED zeigt die hinzuzufügende Funktion OnBnClickedButton1().
void Cpr64410Dlg::OnBnClickedButton1()
{
// TODO: Fügen Sie hier Ihren Kontrollbehandlungscode für die
// Benachrichtigung ein.
UpdateData(TRUE);
// Felder -> Variablen
m_Ausgabe = m_Eingabe1 * m_Eingabe2;
UpdateData(FALSE);
// Variablen -> Felder
}
7. Kompilieren und Test
383
Die Programmiersprache C++
2. Kontrollkästchen, Listenfeld und Kombinationsfeld
Aufgabenstellung:
Mit
Hilfe
des
Anwendungs-Assistenten
soll
eine
dialogfeldbasierende Anwendung erzeugt werden. In dem Dialogfeld befinden sich
zwei statische Textfelder, zwei Kontrollkästchen, ein Listenfeld und eine
Schaltfläche. Nach Speichern, Kompilieren und Starten sollte das Dialogfeld
ungefähr so aussehen:
1. Erstellen der dilogbasierenden Anwendung mit dem MFC-Assistenten.
2.. Erstellen der Ressourcen mit dem Ressourcen-Editor
Nach dem Erstellen der Ressourcen mit dem Ressourcen-Editor und nach
Speichern, Kompilieren, Starten sollte das Ressourcenskript für die
dialogfeldbasierte Anwendung so aussehen:
IDD_PR64440_DIALOG DIALOGEX 0, 0, 286, 199
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE |
WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "Pr64440"
384
Die Programmiersprache C++
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
COMBOBOX
IDC_COMBO1,17,28,145,14,CBS_DROPDOWN | CBS_SORT |
WS_VSCROLL | WS_TABSTOP
LISTBOX
IDC_LIST1,167,28,95,154,LBS_SORT | LBS_NOINTEGRALHEIGHT
|
WS_VSCROLL | WS_TABSTOP
LTEXT
"Dateien",IDC_STATIC,197,15,51,10
LTEXT
"Verzeichnis",IDC_STATIC,52,14,84,10
CONTROL
"Check1",IDC_CHECK1,"Button",BS_AUTOCHECKBOX |
WS_TABSTOP,15,97,11,11
CONTROL
"Check2",IDC_CHECK2,"Button",BS_AUTOCHECKBOX |
WS_TABSTOP,16,120,10,9
LTEXT
"nur Hidden Files",IDC_STATIC,34,96,65,11
LTEXT
"nur Exe-Files",IDC_STATIC,31,119,55,9
PUSHBUTTON
"Dateien zeigen",IDC_BUTTON1,15,142,86,16
END
Das Kombinationsfeld (Combobox) ist eine Kombination aus Eingabefeld und
Listenfeld (Listbox). Das Kontrollkästchen gehört zur MFC-Klasse CButton.
Über das Kombinationsfeld soll eine Verzeichnis-Auswahl erfolgen. Das geschieht
durch Erzeugen folgender Member-Variablen:
Steuerelement-IDs
IDC_CHECK1
IDC_CHECK2
IDC_COMBO1
IDC_COMBO2
IDC_LIST1
IDC_LIST1
Typ
BOOL
BOOL
CString
CComboBox
CString
CListBox
Element (Member-Variable)
m_bCheck1
m_bCheck2
m_strCombo1
m_ctlCombo1
m_strList1
m_ctlList1
3. Ausstatten der Steuerelemente mit Funktionalität
- Hinzufügen der Member-Funktion OnBnClickedButton1() (Klick auf die Schaltfläche)
void Cpr64440Dlg::OnBnClickedButton1()
{
// TODO: Fügen Sie hier Ihren Kontrollbehandlungscode
// für die Benachrichtigung ein.
m_ctlList1.ResetContent();
// loescht den gesamtem Listeninhalt
UpdateData(TRUE);
// Uebertraegt die aktuellen Zustaende (TRUE und FALSE) der
// Kontrollkaestchen in die Variablen m_bCheck1 und m_bCheck2
// und die Eingabe der Kombinationsliste in m_strCombo1
if ((m_bCheck1 == TRUE) && (m_bCheck2 == TRUE))
m_ctlList1.Dir(DDL_HIDDEN | DDL_EXCLUSIVE,m_strCombo1+"\\*.exe");
if ((m_bCheck1 == TRUE) && (m_bCheck2 == FALSE))
m_ctlList1.Dir(DDL_HIDDEN | DDL_EXCLUSIVE,m_strCombo1+"\\*.*");
if ((m_bCheck1 == FALSE) && (m_bCheck2 == TRUE))
m_ctlList1.Dir(DDL_DIRECTORY,m_strCombo1+"\\*.exe");
if ((m_bCheck1 == FALSE) && (m_bCheck2 == FALSE))
m_ctlList1.Dir(DDL_DIRECTORY,m_strCombo1+"\\*.*");
long anzahl = m_ctlList1.GetCount();
CString str;
str.Format("Anzahl %i",anzahl);
CClientDC dc(this);
dc.TextOut(20,120,"
");
dc.TextOut(20,120,str);
}
- Hinzufügen einer Funktion, die auf "Doppelklick"-Nachricht reagiert
385
Die Programmiersprache C++
void Cpr64440Dlg::OnLbnDblclkList1()
{
// TODO: Fügen Sie hier Ihren Kontrollbehandlungscode
// für die Benachrichtigung ein.
UpdateData(TRUE);
WinExec(m_strList1, SW_NORMAL);
// Dateien, die nicht selbst starten koennen, koenne
// innerhalb von WinEWxec mit dem Explorer aufgerufen werden
/*
WinExec("explorer " + m_strCombo1 + "\\" + m_strList1, SW_NORMAL);
*/
}
4. Nach dem Kompilieren und Test können .exe-Files geladen werden.
386
Die Programmiersprache C++
387
Die Programmiersprache C++
3. Bearbeitung von Dateien
Aufgabenstellung: Entwerfe eine dialogfeldbasierende Anwendung nach folgender
Vorlage:
IDD_PR64450_DIALOG DIALOGEX 0, 0, 320, 230
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE |
WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "Datei Lesen und Schreiben"
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
PUSHBUTTON
"Datei Lesen",IDC_BUTTON1,15,7,77,17
EDITTEXT
IDC_EDIT1,15,28,286,70,ES_MULTILINE | ES_AUTOHSCROLL |
ES_READONLY
EDITTEXT
IDC_EDIT2,15,131,282,92,ES_MULTILINE | ES_AUTOHSCROLL |
ES_WANTRETURN
PUSHBUTTON
"Datei Schreiben",IDC_BUTTON2,15,105,75,17
END
Abb. Dialog mit zwei Eingabefeldern und zugehöriger Dialog-Ressource
Die beiden Eingabefelder ermöglichen mehrzeilige Eingaben (ES_MULTILINE). Das
obere Eingabefeld ist zur Ausgabe schreibgeschützt (ES_READONLY), das untere
erlaubt bei Eingaben die Verwendung von (einfachen) Return (ES_WANTRETURN).
Strg + Return bewirkt Zeilenumbruch.
Lösungsschritte:
1. Erstelle die dialogfeldbasierende Anwendung, die das Dialogfeld mit dem MFC-Anwendungsassistenten erzeugt.
2. Erzeuge folgende Member-Variablen
Steuerelement_ID
Variablen-Typ
Element (Member-Variable)
388
Die Programmiersprache C++
IDC_EDIT1
IDC_EDIT2
CString
CString
m_strEdit1
m_strEdit2
3. Durch Doppelklick auf die Schaltflächen erzeuge die Member-Funktionen
OnBnClickedButton1() und OnBnClickedButton2().
4. Fülle die beiden Member-Funktionen mit folgenden Programmquellcode-Zeilen auf:
void Cpr64450Dlg::OnBnClickedButton1()
{
// TODO: Fügen Sie hier Ihren Kontrollbehandlungscode
// für die Benachrichtigung ein.
TCHAR183 str[1000];
CFile datei("D:\\dok\\pgc\\ss02\\projekt\\pr64450\\pr64450Dlg.cpp",
CFile::modeRead);
datei.Read(str,sizeof(str));
datei.Close();
m_strEdit1 = str;
UpdateData(FALSE); // Variablen --> Felder
}
void Cpr64450Dlg::OnBnClickedButton2()
{
// TODO: Fügen Sie hier Ihren Kontrollbehandlungscode
// für die Benachrichtigung ein.
TCHAR str[1000];
UpdateData(TRUE); // Felder -> Variablen
_tcscpy(str,m_strEdit2); // kopiert m_strEdit2 in str
CFile datei("D:\\dok\\pgc\\ss02\\projekt\\pr64450\\demo.txt",
CFile::modeCreate | CFile::modeWrite);
datei.Write(str,sizeof(str));
datei.Close();
}
5. Die im Programm festgelegten Pfad- und Dateinamen sollen flexibel gehandhabt werden. Für solche
immer wiederkehrende Aufgaben werden Standarddialoge zur Verfügung gestellt:
Standardaufgabe
Dateiauswahl
Farbauswahl
Schriftauswahl
Suchen / Ersetzen
Einrichten von Seiten zum Drucken
Drucken
MFC-Klasse
CFileDialog
CColorDialog
CFontDialog
CFindReplaceDialog
CPageSetupDialog
CPrintDialog
Einbinden von CFileDialog in die Schreibroutine:
void Cpr64450Dlg::OnBnClickedButton2()
{
// TODO: Fügen Sie hier Ihren Kontrollbehandlungscode
// für die Benachrichtigung ein.
TCHAR str[1000];
UpdateData(TRUE); // Felder -> Variablen
_tcscpy(str,m_strEdit2); // kopiert m_strEdit2 in str
CFileDialog m_dlgFile(FALSE);
// TRUE: Datei oeffnen FALSE: Datei speichern
if (m_dlgFile.DoModal() == IDOK)
{
m_pathname = m_dlgFile.GetPathName();
}
183
TCHAR umfasst wchar_t* (Unicode) als auch char* (ANSI). Die Zuweisung von TCHAR zu CString kann
man mit dem Zuweisungsoperator erledigen, für den umgekehrten Weg benötigt man die String-Kopierfunktion
_tcscpy(Zieladresse,Quelladresse). Anstelle des Unicode-portablen _tcscpy kann man auch strcpy einsetzen.
389
Die Programmiersprache C++
CFile datei(m_pathname,CFile::modeCreate | CFile::modeWrite);
datei.Write(str,sizeof(str));
datei.Close();
}
Damit der vorliegende Programmcode funktioniert, muß noch die Member-Variable m_pathname vom
Typ CString mit Zugriffstatus private deklariert werden. Mit Hilfe von CFileDialog
m_dlgFile(FALSE); wird ein Objekt der Klasse CFileDialog erzeugt. Durch den Parameter FALSE
wird der Dialog zum Speichern einer Datei erzeugt. m_dlgFile.DoModal() zeigt den Standarddialog
in modaler Form an, d.h. der Dialog muß zunächst abgearbeitet werden, bevor man zur ursprünglichen
Anwendung zurückkehren kann. Über if (m_dlgFile.DoModal() == IDOK) wird der
Rückgabewert der Funktion DoModal() abgefragt. Ist dieser durch Drücken der Schaltfläche gleich
IDOK, dann wird mit m_pathname = m_dlgFile.GetPathName(); der im Standarddialog
ausgewählte Pfad- und Dateiname übernommen.
Einbinden von CFileDialog zum Einlesen von Dateien:
void Cpr64450Dlg::OnBnClickedButton1()
{
// TODO: Fügen Sie hier Ihren Kontrollbehandlungscode
// für die Benachrichtigung ein.
TCHAR str[1000];
CFileDialog m_dlgFile(TRUE);
if (m_dlgFile.DoModal() == IDOK)
{
m_pathname = m_dlgFile.GetPathName();
}
CFile datei(m_pathname,CFile::modeRead);
datei.Read(str,sizeof(str));
datei.Close();
m_strEdit1 = str;
UpdateData(FALSE); // Variablen --> Felder
}
390
Die Programmiersprache C++
6.5 Assistenten und MFC-Anwendungen
6.5.1 Die Assistenten
MFC-Anwendungsassistent
Der MFC-Anwendungsassistent legt ein direkt kompilierbares und ausführbares
MFC-Projekt an und erlaubt es dem Programmierer über eine Reihe von
Dialogfeldern, das zu erzeugende Projekt in vielfältiger Weise anzupassen und
bspw. mit
-
Doc/View-Unterstützung
Datenbankanbindung
OLE- und ActiveX-Features
verschiedene Fensterdekorationen
und anderem
auszustatten.
Der Aufruf des Anwendungsassistenten erfolgt über den Befehl DATEI/NEU. Auf der
Seite PROJEKTE kann der Anwendungsassistent dann aus der Liste der
Projekttypen und Assistenten ausgewählt werden. Klickt man auf OK, erscheint das
Dialogfeld des Assistenten. Das Dialogfeld verfügt über mehrere Optionen.
Abb.:
391
Die Programmiersprache C++
Unter Anwendungstyp wird der grundlegende Typ der Anwendung festgelegt:
EINZELNES DOKUMENT (SDI), eine Anwendung in einem Rahmenfenster, in dem nur ein
Dokumentfenster zur Zeit angezeigt werden kann.
MEHRERE DOKUMENTE (MDI), eine Anwendung mit einem Rahmenfenster, in dem mehrere
Dokumentfenster verwaltet werden können.
DIALOGBASIEREND, eine Anwendung mit einem Dialogfeld als Rahmenfenster.
Als Sprache für die anzulegende Ressource fällt die Wahl auf DEUTSCH.
Man kann auf die DOC/VIEW-Unterstützung verzichten. Doc/View ist ein Programmiermodell, das sich
auf die Konzeption eines Programms bezieht und die Idee propagiert, daß für bestimmte
Anwendungen die Trennung der Daten (Doc) und deren Darstellung(View) Vorteile bringt.
Abb.
Unter Unterstützung für Verbunddokumente wird angegeben, ob die Anwendung
OLE-Verbundfunktionalität als SERVER, CONTAINER, MINISERVER oder
CONTAINER/SERVER unterstützen soll. Nach der Entscheidung für eine Form der
Unterstützung von Verbunddokumenten, kann danach die Unterstützung für
Verbunddateien aktiviert werden.
Außerdem kann dem Projekt Unterstützung für AUTOMATIONS-SERVER sowie für
ACTIVEX-STEUERELEMENT-CONTAINER hinzugefügt werden.
392
Die Programmiersprache C++
Abb.
Abb.
393
Die Programmiersprache C++
Unter Datenbankunterstützung kann die Anwendung mit einer Datenbank verbunden
werden. Falls die Entscheidung für eine Datenbankanbindung gefallen ist, dann kann
im unteren Teil des Dialogfelds über den Schalter Datenquelle eine Datenbank
ausgewählt werden.
Abb.
Unter Benutzeroberflächenfeatures werden alle Einstellungen zum Erscheinungsbild
der Anwendung vorgenommen. Auf dieser Seite kann entschieden werden,
-
ob eine SYMBOLLEISTE, passend zum Menü, gewünscht wird
ob am unteren Rand des Fensters eine STATUSLEISTE angezeigt werden soll (in der automatisch
kurze Hilfetexte zu den Befehlen der Menü- und Symbolleiste angezeigt werden).
welches Aussehen die Fenster haben sollen
394
Die Programmiersprache C++
Abb.
Abb.
395
Die Programmiersprache C++
Zuletzt werden die zu generierenden Klassen angezeigt:
Abb.
Abfangen von Meldungen mit Hilfe des MFC-Klassenassistenten: Meldungstabellen
sind mit Unterstützung durch Klassenansicht und Eigenschaftsfenster zu erstellen.
Klassenansicht
Die Klassenansicht wird über ANSICHT/KLASSENANSICHT aufgerufen
Die Meldungen-Schaltfläche des Eigenschaftsfensters.
Zur Tabelle der Meldungen gelangt man durch Auswahl einer Klasse, Anzeigen des
zur Klasse zugehörigen Eigenschaftsfensters (z. B. das ANSICHT-Menü) und Klick
in der Symbolleiste des Eigenschaftsfensters auf die Schaltfläche MELDUNGEN
(befindet sich zwischen dem gelben Blitz und der grünen Raute).
396
Die Programmiersprache C++
6.5.2 Das Doc/View-Modell
Beim Doc/View-Modell handelt es sich nicht um ein konkretes Element einer
Windows-Anwendung, sondern lediglich um einen bestimmten Programmteil (, der
allerdings in der MFC-Programmierung eine große Rolle spielt). Auf der einen Seite
stehen die eigentlichen Rohdaten, repräsentiert durch die Dokumentenklasse, die
von der Basisklasse CDocument abgeleitet ist. Auf der anderen Seite steht die
Ansichtsklasse, die für die Anzeige der Daten verantwortlich ist und auf CView
basiert. Für die gleichen Daten (also ein CDocument-Objekt) können mehrere
Ansichten eingerichtet werden, in den Daten auf jeweils unterschiedliche Weise
angezeigt werden.
Dokument, Ansicht und Dokumentenvorlage
Die Dokumentklasse eines Projektes ist von der MFC Klasse CDocument
abgeleitet. CDocument stellt die grundlegende Funktionalität für eine
benutzerdefinierte Dokumentklasse zur Verfügung. Eine der nützlichsten Funktionen,
die sich in einem Dokument befindet, ist die MFC Funktion Serialize(CArchive&
ar). Mit Hilfe dieser Funktion können die Daten in einem Dokument relativ einfach
serialisiert werden. Ein Dokument stellt den Teil der Daten dar, der typischerweise
mit dem Befehl Datei Öffnen geöffnet und dem Befehl Datei Speichern, gespeichert
wird. CDocument unterstützt die Standard Operationen wie Erstellen, Laden und
Speichern eines Dokuments. Das Anwendungsgerüst reagiert automatisch auf die
Befehle Datei Öffnen und Datei Speichern und ruft die bekannten Windows Dialoge
für diesen Zweck auf.
Die Ansichtsklasse einer Anwendung ist von der MFC Klasse CView abgeleitet.
Diese Klasse bietet die grundlegende Funktionalität für eine benutzerdefinierte
Ansichtsklasse. Eine Ansicht ist einem bestimmten Dokument zugeordnet und dient
als eine Art Vermittler zwischen dem Dokument und Benutzer. Die Ansicht gibt ein
Abbild des Dokuments auf dem Bildschirm oder Drucker wieder und interpretiert die
Eingaben des Benutzers als Operationen aufs Dokument. Ein Dokument kann unter
Umständen mehrere Ansichten haben, eine Ansicht ist jedoch nur einem einzigen
Dokument zugeordnet.
Die Klasse CView hat mehrere abgeleitete Klassen, die man als Basis für die
Ansichtsklasse verwenden kann:
Klasse
CEditView
CFormView
CHtmlView
CListView
CRichEditView
CSrollView
CTreeView
Beschreibung
liefert die Funktionalität eines Eingabefelds. Mit dieser Klasse lassen sich einfache
Text-Editoren implementieren.
Die Basisklasse für Ansichten, die Steuerelemente enthalten. Mit dieser Klasse
lassen sich formularbasierte Dokumente in Anwendungen bereitstellen.
liefert die Funktionalität eines Webbrowsers. Die Ansicht behandelt direkt die URLNavigation, Hyperlinks, usw.
stellt die Funktionalität von Listen in der Dokument-/View-Architektur
realisiert Zeichen- und Absatzformatierungen. Mit dieser Klausel lassen sich
Textverarbeitungen implementieren.
stellt Bildlauffähigkeiten für eine CView-Klasse dar
stellt die Funktionalität von Bäumen in der Dokument-/View-Architektur dar
Abb.:
Wichtige Funktionen der Klassen CDocument und CView:
397
Die Programmiersprache C++
CView::GetDocument()
// GetDocument() liefert einen Zeiger auf ein Dokumentobjekt, das mit der Ansicht assoziiert
// ist. Diese Funktion ermöglicht, auf Member-Funktionen und öffentlichen Variablen des Dokuments
// zuzugreifen. Falls der Wunsch besteht, auch auf die nicht öffentlichen Variablen des Dokuments
// zuzugreifen, muß man die Ansichtsklasse als “friend“ von Dokumentklasse deklarieren
CDocument::SetModifiedFlag()
// Mit dem Aufruf dieser Funktion wird sichergestellt, daß das Anwendungsgerüst (Framework) den
// Benutzer auffordert, die Änderungen zu speichern, bevor er das Dokument schließen kann.
CDocument::OnNewDocument()
// Diese Funktion wird vom AppWizard in die Dokumentklasse erstellt. OnNewDocument() wird vom
// Anwendungsgerüst als Reaktion auf den Befehl Neu des Menübefehls aufgerufen. Hier sollten die
// Daten des Dokuments initialisiert werden.
CDocument::Serialize(CArcive& ar)
// In der Funktion Serialize() kann man mit Hilfe des Parameters ar direkt ins Archive schreiben
// oder davon lesen.
Kommunikation zwischen Dokument und Ansicht
Die Ansicht ist für die Anzeige und Modifikation von Daten verantwortlich, jedoch
nicht für deren Speicherung. Damit überhaupt eine funktionierende Kommunikation
zwischen den beiden Klassen zustande kommen kann, muß das Dokument
geeignete Funktionen und Wege der Ansicht zur Verfügung stellen, womit die
Ansicht auf die Daten des Dokuments zugreifen kann. Mit der Funktion
GetDocument() erfolgt der Zugriff auf die Daten des Dokuments. Die Beziehung
zwischen der Dokumentklasse, Ansichtsklasse und Hauptrahmenfensterklasse wird
in der sogenannten Dokumentvorlage in der Funktion InitInstance() der
Anwendungsklasse festgehalten. Die Anwendungsklasse befindet sich in der Datei,
die den Namen Ihres Projektes trägt. Der AppWizard übernimmt die Initialisierungen
für die Anwendung in dieser Funktion. Der Anwender hat normalerweise mit der
Anwendungsklasse nichts zu tun.
Bsp.: Anzeige eines über eine Dialogbox angegebenen Satzes in der Ansicht einer
SDI-Anwendung184.
Der Text im folgenden Fenster sagt dem Anwender, was zu tun ist.
Nach Drücken der „OK“-Schaltfläche erscheint das Fenster der Ansichtsklasse.
184
pr65220
398
Die Programmiersprache C++
Ein Klick auf die linke Maustaste bewirkt die Anzeige der folgenden Dialogseite:
Hier kann ein Satz eingegeben werden, der anschließend (nach Betätigen der Schaltfläcke „OK“) im
Dialogfenster im Fenster der Ansicht wiedergegeben wird.
1. Anlegen eines neuen SDI-Projekts
2. Entwurf einer Dialogseite für die Eingabe eines Datensatzes. Auf der Dialogseite befindet sich
neben „OK“ und „Abbrechen“ eine Editbox mit der Member-Variable m_satz.
Erzeugen der Dialogressource
- Aufruf des Befehls PROJEKT/RESOURCE HINZUFUEGEN
399
Die Programmiersprache C++
- Doppelklick im Feld RESSOURCENTYP auf "Dialog". Der Dialog-Editor erscheint im Fenster von
Visual Studio.
Öffne
das
Eigenschaftsfenster
für
das
neue
Dialogfeld
(über
ANSICHT/EIGENSCHAFTSFENSTER). Ändere den Titel in "Beispieldialog".
3. Erstelle mit Hilfe des Klassenassistenten
- eine Klasse „CDialog1“ zur Präsentation der Dialogseite
Klick mit der rechten Maustaste irgendwo in das Dialogfeld und Auswahl des Befehls KLASSE
HINZUFUEGEN im Kontextmenü. Über den MFC-KLassenassistenten kann die Klasse mit dem
unter 2. erzeugten Dialogfeld verknüpft werden.
Zur Verbindung der Steuerelemente des Dialogfelds mit dem Code müssen in der Dialogklasse
passende Membervariablen verknüpft werden: Aufruf ANSICHT/KLASSENANSICHT, Wahl der
Klasse CDialog1. Danach entweder VARIABLE HINZUFÜGEN aufrufen oder mit der rechten
Maustaste das Kontextmenü öffnen und dort HINZUFÜGEN/VARIABLE HINZUFÜGEN anwählen.
Man kann auch, wenn bereits eine Klasse für das Dialogfeld erstellt ist, irgendwo in das Dialogfeld
klicken und dann im Kontextmenü VARIABLE HINZUFÜGEN auswählen. Die Variable wird dem
Eingabefeld IDC_EDIT1 unter dem Namen m_satz zugeordnet.
- eine Nachrichten-Funktion für die Nachricht WM_LBUTTONDOWN in der Ansicht (OnLButtonDown)
4. Mache die Dialogseite in der Ansicht durch Eintragen der Header-Datei der Dialogseite (mit
Hilfe von include) bekannt
5. Deklariere in Cpr65220Doc eine öffentliche Variable mit dem Namen „satz“ vom Typ CString.
6. Quellcode-Ergänzungen.
// Cpr65220View Meldungshandler
void Cpr65220View::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein,
//
und/oder benutzen Sie den Standard.
CDialog1 dlg;
int iresult=dlg.DoModal();
if(iresult == IDOK)
{
Cpr65220Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// Inhalt der Editbox uebergeben
pDoc->satz = dlg.m_satz;
// Sicherstellen, dass der Benutzer beim Verlassen der Anwendung
// zum Speichern der Änderungen aufgefordert wird.
pDoc->SetModifiedFlag();
// Einleiten Zeichenvorgang
Invalidate();
}
CView::OnLButtonDown(nFlags, point);
}
Für die Speicherung von “satz” muß in der Funktion Serialize() gesorgt werden
400
Die Programmiersprache C++
// Cpr65220Doc Serialisierung
void Cpr65220Doc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: Hier Code zum Speichern einfügen
ar << satz;
}
else
{
// TODO: Hier Code zum Laden einfügen
ar >> satz;
}
}
Die Funktion OnDraw() übernimmt die Darstellung des Satzes.
void Cpr65220View::OnDraw(CDC* pDC)
{
Cpr65220Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
pDC->TextOut(50,50,pDoc->satz);
}
Das Fenster der Ansicht kann zu Beginn einen Hinweis auf den darzustellenden Satz enthalten. Platz
für eine derartige Initialisierung stellt die Funktion OnNewDocument() im Dokument bereit.
BOOL Cpr65220Doc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
// TODO: Hier Code zur Reinitialisierung einfügen
// (SDI-Dokumente verwenden dieses Dokument)
satz = "Hier kann auch Ihr Satz stehen.";
// Titel fuer den Dialog
SetTitle("Neuer Titel fuer den Dialog");
return TRUE;
}
Ein Name für das Dokument kann in der Dokumentenklasse unter OnNewDocument() mit der MFCFunktion CDocument::SetTitle(LPCSTR lpszTitel) bestimmt werden.
Initialisierungen für die Ansicht erfolgen in der Funktion OnInitialUpdate(). Immer, wenn der
Befehl NEU oder ÖFFNEN vom Dateimenü aus, betätigt wird, dann wird OnInitialUpdate() mit
Informationen über die Anwendung in einer Message Box aufgerufen. Damit diese Message Box nicht
bei jedem Öffnen einer neuen Datei auf dem Bildschirm erscheint, wurde im Konstruktor der Ansicht
die BOOL-Variabe „Meldung“ initialisiert.
void Cpr65220View::OnInitialUpdate(void)
{
CView::OnInitialUpdate();
if (Meldung)
{
MessageBox(
"Es handelt sich hier um ein einfaches Programm für"
" Serialisierung. Wenn Sie im Programm"
" auf die linke Maustaste drücken, erscheint der Dialog"
" zum Eingabe eines Satzes. Diesen Satz können Sie"
" speichern und nochmals laden."
);
Meldung=FALSE;
}
401
Die Programmiersprache C++
}
Die Geometrie des Hauptfensters wird über CMainFrame::PreCreateWindow(...) beeiflußt:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
// TODO: Ändern Sie hier die Fensterklasse oder die Darstellung,
// indem Sie CREATESTRUCT cs modifizieren.
cs.x = 100;
cs.y = 100;
cs.cx = 400;
cs.cy = 200;
cs.style = WS_OVERLAPPED | WS_CAPTION | FWS_ADDTOTITLE
| WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU;
return TRUE;
}
402
Die Programmiersprache C++
Dokumente und Ansichten
Der Begriff Ansichten (View) gibt einen Hinweis auf die bildhafte Darstellung von
Daten. Der Begriff Dokumente (Doc) ist nicht selbsterklärend. "Doc" enthält die
Daten (Zahlen, Texte, ...). "View" stellt die Daten bildlich dar. "Doc" stellt auch einen
Mechnismus zur Verfügung, der für das Lesen und Schreiben der Daten in Dateien
sorgt ("Serialisierung"). Dafür bietet View die Möglichkeit, die Ansicht der Daten nicht
nur auf dem Bildschirm, sondern auch z.B. auf einem Drucker auszugeben.
Außerdem stellt eine View den Kontakt mit den Benutzereingaben her (Maus-,
Tastatureingaben, ...).
Für den Rahmen um "Doc" und "View" sorgt eine Rahmenfensterklasse, z.B.
CFrameWnd. "Doc" beruht auf der Klasse CDocument, "View" auf der von CWnd
abgeleiteten Klasse CView. Der Wirkungsmechanismus der Dokumentvorlagen ist in
der abstrakten MFC-Klasse CDocTemplate enthalten. Diese Klasse wird nicht direkt
benutzt, sondern die davon abgeleitete Klassen CSingleDocTemplate für SDI und
CMultiDocTemplate für MDI. Zusätzlich benötigt man eine von CWinApp
abgeleitete Klasse.
6.5.3 SDI- und MDI-Anwendungen
Eine SDI-Anwendung ist eine dokumentenbezogene Anwendung, die nur mit einem
Dokument zu einem bestimmten Zeitpunkt und nur mit einem Typ von Dokument
arbeiten kann.
Eine MDI-Anwendung ist ebenfalls eine dokumentenbezogene Anwendung, bei der
der Benutzer an mehreren Dokumenten gleichzeitig arbeiten und zwischen den
Fenstern der Anwendung umschalten kann.
403
Die Programmiersprache C++
6.5.3.1 SDI-Anwendungen
1. Zeichnen eines Würfels
1. Anlegen eines neuen MFC-Anwendung-Visual-C++-Projekts
2. Im Bereich ANWENDUNGSTYP wähle die Option EINFACHES DOKUMENT
Abb.
3. Im Bereich ZEICHENFOLGEN FUER DOKUMENTENVORLAGEN gib, wie die folgende Abb. zeigt,
eine Dateinamenerweiterung für die Dateien an, die die Anwendung erzeugen wird.
404
Die Programmiersprache C++
4. Im Bereich ERSTELLTE KLASSEN kann eine Basisklasse ausgewählt werden, auf der die
Ansichtsklasse basieren soll. Wird die Einstellung CView beibehalten, kann direkt auf FERTIG
STELLEN geglickt werden. Der MFC-Anwendungsassistent erzeugt das Anwendungsgerüst. Man
erhält nach dem Ausführen folgendes Fenster:
405
Die Programmiersprache C++
Unter dem Menü "Datei" wurden bereits die wesentlichen Möglichkeiten in Bezug auf die ".sav"Dateien berücksichtigt. Der Menüpunkt "Bearbeiten" ist im Moment nicht aktuell. Unter Hilfe ergibt sich
die bekannte About-Dialogbox.
5. In das vorliegende Fenster kann jetzt der Würfel eingezeichnet werden. Man zeichnet dazu ein
erstes Quadrat, dahinter schräg versetzt ein zweites Quadrat, und verbindet die Eckpunkte mit vier
diagonalen Linien.
Quadrate und Würfel haben gleiche Seitenlängen. Die beiden Quadrate haben beide einen linken
oberen Eckpunkt, der jeweils eine x- und eine y-Koordinate besitzt. Die zugehörigen Daten müssen in
die Dokumentenklasse eingefügt werden.
double laenge;
CPoint erstesQuadrat;
CPoint zweitesQuadrat;
Die Deklaration erfolgt in der Dokumentenklasse , d.h. in Cpr65310.h.
// pr65310Doc.h : Schnittstelle der Klasse Cpr65310Doc
//
#pragma once
class Cpr65310Doc : public CDocument
{
...
// Implementierung
public:
CPoint erstesQuadrat;
CPoint zweitesQuadrat;
int laenge;
virtual ~Cpr65310Doc();
...
};
Diese Member-Variablen werden an zwei Stellen der Dokumentenklasse initialisiert:
- im Konstruktur der Dokumentenklasse
- in der Member-Funktion "OnNewDocument" der Dokumentenklasse
// pr65310Doc.cpp : Implementierung der Klasse Cpr65310Doc
...
// Cpr65310Doc Erstellung/Zerstörung
Cpr65310Doc::Cpr65310Doc()
{
// TODO: Hier Code für One-Time-Konstruktion einfügen
laenge = 200;
erstesQuadrat.x = 100;
erstesQuadrat.y = 200;
zweitesQuadrat.x = 200;
zweitesQuadrat.y = 100;
}
Cpr65310Doc::~Cpr65310Doc()
{
}
BOOL Cpr65310Doc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
// TODO: Hier Code zur Reinitialisierung einfügen
// (SDI-Dokumente verwenden dieses Dokument)
406
Die Programmiersprache C++
laenge = 200;
erstesQuadrat.x = 100;
erstesQuadrat.y = 200;
zweitesQuadrat.x = 200;
zweitesQuadrat.y = 100;
return TRUE;
}
6. Daten anzeigen. Die Anzeige der Daten wird in der Ansichtsklasse realisiert. Die grafische Anzeige
erfolgt in der Member-Funktion OnDraw(). Diese Funktion bietet bisher folgendes:
void Cpr65310View::OnDraw(CDC* /*pDC*/)
{
Cpr65310Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
}
Wichtig sind die beiden Zeiger pDC und pDoc. Der erste Zeiger zeigt auf einen Device Context, der
beim Neuzeichnen des Fensters eingesetzt wird, und der zweite zeigt auf die Dokumentenklasse
Cpr65310Doc. Für OnDraw() ergibt sich folgende Programmquellcode:
void Cpr65310View::OnDraw(CDC* pDC)
{
Cpr65310Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
// Erstes Quadrat zeichnen
pDC->MoveTo(pDoc->erstesQuadrat);
pDC->LineTo(pDoc->erstesQuadrat.x + pDoc->laenge,
pDoc->erstesQuadrat.y);
pDC->LineTo(pDoc->erstesQuadrat.x + pDoc->laenge,
pDoc->erstesQuadrat.y + pDoc->laenge);
pDC->LineTo(pDoc->erstesQuadrat.x ,
pDoc->erstesQuadrat.y + pDoc->laenge);
pDC->LineTo(pDoc->erstesQuadrat.x ,pDoc->erstesQuadrat.y);
// Zweites Quadrat zeichnen
pDC->MoveTo(pDoc->zweitesQuadrat);
pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge,
pDoc->zweitesQuadrat.y);
pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge,
pDoc->zweitesQuadrat.y + pDoc->laenge);
pDC->LineTo(pDoc->zweitesQuadrat.x ,
pDoc->zweitesQuadrat.y + pDoc->laenge);
pDC->LineTo(pDoc->zweitesQuadrat.x ,
pDoc->zweitesQuadrat.y);
// Verbindungslinien zeichnen
pDC->MoveTo(pDoc->erstesQuadrat);
pDC->LineTo(pDoc->zweitesQuadrat);
pDC->MoveTo(pDoc->erstesQuadrat.x + pDoc->laenge,
pDoc->erstesQuadrat.y);
pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge,
pDoc->zweitesQuadrat.y);
pDC->MoveTo(pDoc->erstesQuadrat.x + pDoc->laenge,
pDoc->erstesQuadrat.y + pDoc->laenge);
pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge,
pDoc->zweitesQuadrat.y + pDoc->laenge);
pDC->MoveTo(pDoc->erstesQuadrat.x,
pDoc->erstesQuadrat.y + pDoc->laenge);
pDC->LineTo(pDoc->zweitesQuadrat.x,
pDoc->zweitesQuadrat.y + pDoc->laenge);
}
407
Die Programmiersprache C++
Nach dem Ausführen des Programms zeigt das Fenster folgendes Aussehen:
Abb.: View des perspektifischen Würfels
7. Schreiben / Lesen von Dokumentobjekten (sog. Serialisierung).
Der Assistent hat bereits eine Funktion zur Serialisierung der Daten bereit gestellellt:
// Cpr65310Doc Serialisierung
void Cpr65310Doc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: Hier Code zum Speichern einfügen
}
else
{
// TODO: Hier Code zum Laden einfügen
}
}
Mit Serialisierung ist hier das Speichern und Laden der Daten im Zusammenhang mit ".sav" –Dateien
gemeint. Fünf Daten (laenge, erstesQuadrat, zweitesQuadrat) werden übergeben:
// Cpr65310Doc Serialisierung
void Cpr65310Doc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: Hier Code zum Speichern einfügen
ar << erstesQuadrat;
ar << zweitesQuadrat;
}
else
408
Die Programmiersprache C++
{
//
ar
ar
ar
TODO: Hier Code zum Laden einfügen
>> laenge;
>> erstesQuadrat;
>> zweitesQuadrat;
}
}
Die Daten werdem seriell in einer Datei abgelegt. Daher müssen lögischerweise die Daten immer in
der gleichen Variablen-Reihenfolge gelesen bzw. geschrieben werden.
In der Klasse CArchive existieren die überladenen Operatoren << und >>, mit denen auch
zusammengesetzte Daten (z.B. CPoint) einfach hin- und herschieben kann.
8. Basteln einer Routine, mit der man das erste Quadrat verschieben kann. Über die Klassenansicht
zu Cpr65310View aus der Eigenschaftsliste WM_KEYDOWN die Funktion OnKeyDown() hinzufügen.
Es wird eine neue Funktion OnKeyDown() mit folgendem Aussehen generiert:
// Cpr65310View Meldungshandler
void Cpr65310View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, und/oder
benutzen Sie den Standard.
CView::OnKeyDown(nChar, nRepCnt, nFlags);
}
Über den ersten Parameter wird eine switch-Anweisung konstruiert. Je Tastendruck soll das erste
Quadrat um 10 Pixel in die entsprechende Richtung verschoben werden.
409
Die Programmiersprache C++
void Cpr65310View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, und/oder
benutzen Sie den Standard.
Cpr65310Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
switch(nChar)
{
case 37: (pDoc->erstesQuadrat.x) -= 10;
break;
case 38: (pDoc->erstesQuadrat.y) -= 10;
break;
case 39: (pDoc->erstesQuadrat.x) += 10;
break;
case 40: (pDoc->erstesQuadrat.y) += 10;
break;
}
// Mitteilung ueber die veraenderten Daten an die
// Dokumentenklasse
pDoc->SetModifiedFlag();
// Aktualisierung der Ansichtsklasse
Invalidate();
CView::OnKeyDown(nChar, nRepCnt, nFlags);
}
Das Setzen des "ModifiedFlag" führt dazu, dass die Anwendung bei Veränderungen (mit den
Pfeiltasten) vor dem Beenden oder Neuladen nach dem Speichern der aktuellen Daten befragt.
Abb.: Bewegung des Würfels (zwei der fünf Daten)
410
Die Programmiersprache C++
6.5.3.2 MDI-Anwendungen
MDI-Anwendungen unterscheiden sich von SDI-Anwendungen durch die beiden
(MDI-) spezifischen Klassen CMDIFrameWnd und CMDIChildWnd:
-
-
Die von der CMDIFrameWnd abgeleiteten Klasse CMainFrame bestimmt den Hauptrahmen der
Anwendung. Die Klasse CMDIFrame ist das äußere Rahmenfenster einer MDI-Anwendung.
MDIGetActive ist eine Member-Funktion dieser Klasse, die einen Zeiger auf das momentan aktive
untergeordnete Fenster zurückgibt. Die meisten anderen Funktionen, die mit untergeordneten
Fenstern arbeiten, sind in dem im MDI-Anwendungsrahmen vom Anwendungsassistenten
erstellten Fenstermenü enthalten.
Die von CMDIChildWnd abgeleitete Klasse CChildFrame bildet den Rahmen, der die CViewKlassen aufnimmt. Dieser Rahmen leitet die Nachrichten und Ereignisse an die Ansichtsklasse zur
Verarbeitung oder Anzeige weiter. Die Klasse CMDIChildView ist das innere Rahmenfenster
einer MDI-Anwendung. Die meisten Funktionen dieser Klasse haben mit dem Zustand des
untergeordneten Fensters im übergeordneten Rahmen zu tun: MDIDestroy, MDIActivate,
MDIMaximize, MDIRestore. Keine dieser Funtionen übernimmt Parameter oder gibt Ergebnisse
zurück. GetMDIFrame gibt einen Zeiger auf das übergeordnete CMDIFrameWnd-Fenster zurück.
411
Die Programmiersprache C++
6.5.4 Das MFC-Anwendungsgerüst
6.5.4.1 Erzeugen der Fenster
Bis auf wenige Ausnahmen verfügt jede Windows-Anwendung über ein sichtbares
Hauptfenster. Ein Hauptfenster kann ein Dialogfenster oder ein sog. Rahmenfenster
sein. Rahmenfenster haben einen speziellen Rahmen, in dem bestimmte Dekorationselemente (Menü, Symbolleisten und Statusleiste) integriert werden können.
Der innere Bereich des Fensters, der nach Abzug der Dekorationselemente frei
bleibt, ist der sog. Clientbereich.
Zum Erzeugen eines Rahmenfensters sind folgende Schritte nötig:
1. Zum Erzeugen eines Rahmenfensters wird ein Objekt der Klasse CMainFrame instantiiert:
CMainFrame *pMainWnd = new CMainFrame
2. Zur Verbindung des Rahmenfensters mit der Anwendung wird der Zeiger auf das Fensterobjekt an
die globale MFC-Variable m_pMainWnd übergeben: m_pMainWnd = pMainWnd;
3. Zur Anzeige der Rahmenfenster werden die Methoden ShowWindow() und UpdateWindow()
aufgerufen:
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
Im Quelltext eines mit dem MFC-Anwendungsassistenten erzeugten Programms
wird man diese Anweisungen so nicht wiederfinden. Das liegt an der Doc/ViewUnterstützung der MFC, die mit sogenammten Dokumentvorlagen arbeitet. Bei der
Einrichtung der Dokumentenvorlage wird auch das Rahmenfenster eingerichtet.
6.5.4.2 Anpassen der Fenster
1. Rahmenfenster und Systemmenü ( in einer SDI-Anwendung)
Am einfachsten ist die Anpassung an eigene Vorstellungen durch Aufruf der
entsprechenden Optionen auf den Dialogseiten des Anwendungs-Assistenten. Alle
Einstellungen zum Erscheinungsbild der Anwendung werden im wesentlichen unter
Benutzeroberflächenfeatures vorgenommen. Hier kann entschieden werden,
-
ob eine Symbolleiste, passend zum Menü, bereitgestellt werden soll.
ob am unteren Rand des Fensters eine Statusleiste angezeigt werden soll, in der automatisch
kurze Hilfetexte zu den Befehlen der Menü- und Symbolleiste angezeigt werden.
welches Aussehen die Menüs haben sollen.
Zur Demonstration wird ein Anwendungsbeispiel erstellt:
- Erstelle mit dem Anwendungsassistenten eine neue SDI-Anwendung
- Beachte unter Benutzeroberflächenfeatures die folgenden Einstellungen zu verschiedenen Zutaten
(Symbolleiste, Statusleiste, Systemmenü, Breiter Rahmen, Minimieren- und MaximierenSchaltfläche, etc) für Gestalt und Aussehen des Fensters
412
Die Programmiersprache C++
Mit diesen Einstellungen sieht die Funktion CMainFrame::PreCreateWindow(...) so aus:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
return TRUE;
}
Falls die Minimieren- und Maximieren-Schaltfläche, Breiter Rahmen, andockbare Symbolleiste
abgewählt wurde, sähe das Ergebnis folgendermaßen aus:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
cs.style = WS_OVERLAPPED | WS_CAPTION | FWS_ADDTOTITLE
| WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU;
return TRUE;
}
Der nach der Abwahl verbleibende Rest wird dann cs.style zugewiesen. Der Fenterstil wird durch eine
Kombination bestimmter Konstanten festgelegt.
Stil
WS_BORDER
WS_CAPTION
WS_CHILD
WS_CHILDWINDOW
WS_CLIPCHILDREN
WS_DISABLED
WS_DLGFRAME
Bedeutung
Dünner Rand
Titel und dünner Rand
Kindfenster
Entspricht WS_CHILD
Beim Zeichnen der Elternfensters werden Bereiche des
Kindfensters ausgenommen
Fenster anfänglich deaktiviert
Für Dialogfenster
413
Die Programmiersprache C++
WS_GROUP
WS_HSCROLL
WS_ICONIC
WS_MAXIMIZE
WS_MAXIMIZEBOX
WS_MINIMIZE
WS_MINIMIZEBOX
WS_OVERLAPPED
WS_OVERLAPPEDWINDOW
WS_POPUP
WS_POPUPWINDOW
WS_SIZEBOX
WS_SYSMENU
WS_TABSTOP
WS_THICKFRAME
WS_TILED
WS_TILEDWINDOW
WS_VISIBLE
WS_VSCROLL
Zur Gruppierung von Steuerelementen
Horizontale Bildlaufleiste
Entspricht WS_MINIMIZED
Fenster ist anfänglich maximiert
Schalter zum Maximieren (erfordert WS_SYSMENU, nicht
in Kombination mit WS_EX_CONTEXTHELP)
Fenster ist anfänlich minimiert
Schlter zum Minimieren (erfordert WS_SYSMENU, nicht in
Kombination mit WS_EX_CONTEXTHELP)
Tiielleiste und Rahmen
Kombination der Stile WS_OVERLAPPED, WS_CAPTION,
WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX,
WS_MAXIMIZEBOX
Popup-Fenster
Kombination der Stile WS_BORDER, WS_POPUP,
WS_SYSMENU
Entspricht WS_THICKFRAME
Systemmenü (erfordert WS_CAPTION)
Steuerelement kann mit der Tabulator-Taste angesteuert
werden.
Breiter Rahmen. Nur dieser Rahmen erlaubt das verändern
der Fenstergröße durch Ziehen des Rahmens
Entspricht WS_OVERLAPPED
Entspricht WS_OVERLAPPEDWINDOW
Fenster anfangs sichtbar
Vertiklae Bildlaufleiste
Abb.: Fensterstile für cs.style
Neu ist FWS_ADDTOTITLE, ein Beispiel eines Frame-Window Style.
Frame-Window Style (FWS)
FWS_ADDTOTITLE
FWS_PREFIXTITLE
FWS_SNAPTOBARS
Bedeutung
Der Document-Titel wird dem Namen der Anwendung hinzugefügt
Funktioniert zusammen mit FWS_ADDTOTITLE.
Document-Titel steht vor dem Namen der Anwendung.
Dies ist die Standardeinstellung des MFC-Assistenten.
Steuert die Größe des Rahmenfensters, das eine Steuerleiste
("control bar") als frei bewegliches ("floating") Fenster beherbergt.
Das Rahmenfenster wird der Größe der Steuerleiste angepasst.
Elemente der Rahmenfensters enthält die von der Klasse CFrameWnd abgeleitete Klasse
CMainFrame
class CMainFrame : public CFrameWnd
{
protected: // Nur aus Serialisierung erstellen
CMainFrame();
DECLARE_DYNCREATE(CMainFrame)
public:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
public:
virtual ~CMainFrame();
protected: // Eingebundene Elemente der Steuerleiste
CStatusBar m_wndStatusBar;
CToolBar
m_wndToolBar;
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
414
Die Programmiersprache C++
Der Konstruktor CMainFrame() erzeugt für die Erzeugung des Rahmenfensters, der Destruktor
~CMainFrame() zerstört es. Außerdem enthält die Klasse zwei wichtige Funktionen:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
PreCreateWindow(…) ermittelt einen Rückgabewert vom Typ BOOL. Ist dieser Wert FALSE, dann ist
die Einstellung des Rahmenfensters gescheitert.
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
// TODO: Ändern Sie hier die Fensterklasse oder die Darstellung,
// indem Sie CREATESTRUCT cs modifizieren.
return TRUE;
}
Diese Methode wird automatisch vor Anpassung des eigentlichen Windows-Fenster aufgerufen185.
Dabei wird der Methode die Adresse auf eine Strukturvariable vom Typ CREATESTRUCT übergeben:
typedef struct tagCREATESTRUCT
{
LPVOID lpCreateParams; // Fensterdaten
HANDLE hInstance;
//
HMENU hMenu;
// Menue des Rahmenfensters
HWND
hwndParent;
// uebergeordnetes Fenster
int cy;
// Hoehe
int cx;
// Breite
int y;
// y-Koordinate des oberen Rands
int x;
// x-Koordinate des linken Rands
LONG style;
// Fensterstil
LPCSTR plszName;
// Fenstername
LPCSTR lpszClass;
// Fensterstruktur
DWORD dwExStyle;
// erw. Fensterstil
} CREATESTRUCT;
In PreCreateWindow() kann auf einzelne Elemente dieser Struktur zugegriffen werden, z.B.
Anpassen von Position und Größe des Hauptfensters
1. Anlegen einer neuen SDI-Anwendung mit Unterstützung für Doc/View durch den MFCAnwendungsassistenten.
2. Ausführen des Programms; Beobachten von Position und Größe des Fensters
3. Expandsion des Knoten CMainFrame in der KLASSEN-Ansicht, Doppelklick auf die Methode
CreateWindow()
4. Den Koordinaten x und y für die obere linke Ecke des Fensters sollen neue Werte zugewiesen
werden. Den Elementen cx und cy, die Höhe und Breite des Fensters bestimmen, sollen ebenfalls
neue Werte zugewiesen werden.
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
// ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse oder das
// Erscheinungsbild, indem Sie CREATESTRUCT cs modifizieren.
cs.x = 100;
cs.y = 100;
cs.cx = 400;
cs.cy = 100;
return TRUE;
}
185
Kurz bevor das Anwendungsgerüst intern die Methode Create() aufruft.
415
Die Programmiersprache C++
OnCreate() zeigt die Erstellung der Symbolleiste, der Statusleiste und den Anweisungsblock, der sich
mit der Andockbarkeit der Symbolleiste beschäftigt:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return -1;
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE
| CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY |
CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Symbolleiste konnte nicht erstellt werden\n");
return -1;
// Fehler bei Erstellung
}
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Statusleiste konnte nicht erstellt werden\n");
return -1;
// Fehler bei Erstellung
}
// TODO: Löschen Sie diese drei Zeilen, wenn Sie nicht möchten,
// dass die Systemleiste andockbar ist
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
return 0;
}
Hier ist offensichtlich der Platz zur Vornahme von Veränderungen, z.B.:
1. Keine andockbare Symbolleiste
Wie durch den Kommentar angegeben, müssen die drei letzten Anweisungen (vor dem return)
gestrichen werden. Die Symbolleiste hängt danach fest.
2. Keine Symbolleiste
Die Erzeugung wird gestrichen
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE
| CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY |
CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Symbolleiste konnte nicht erstellt werden\n");
return -1;
// Fehler bei Erstellung
}
3. Keine Statusleiste, jedoch mit Symbolleiste
416
Die Programmiersprache C++
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Statusleiste konnte nicht erstellt werden\n");
return -1;
// Fehler bei Erstellung
}
4. Keine Statusleiste, keine Symbolleiste und vor allem kein Menü.
Das Handle auf das Menü war cs.hMenu, die richtige Funktion ist CMainFrame::PreCreateWindow(...)
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if (cs.hMenu != NULL)
{
::DestroyMenu(cs.hMenu); // Geladenes Menü entfernen
cs.hMenu = NULL;
// Rahmenfenster hat kein Menü
}
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
return TRUE;
}
5. Schließen per Mausklick unterbinden (nach dem Wiederherstellen des Fensters in ursprünglicher
Gestalt).
Der richtige Ort für das Deaktivieren von "X" im Systemmenü ist die Funktion
CMainFrame::OnCreate():
CMenu* pSystemMenu = GetSystemMenu(FALSE);
pSystemMenu->EnableMenuItem(SC_CLOSE, MF_GRAYED);
Mit der Funktion CMenu* CWnd :: GetSystemMenu(BOOL bRevert) const wird ein Zeiger auf
das System-Menü-Objekt beschafft. Der Parameter muß FALSE gesetzt sein, damit ein Zeiger auf
eine manipulierbare Kopie von CMenu erhalten wird. Mit dem Zeiger können verschiedene Funktionen
der
Klasse
CMenu
angewendet
werden.
Im
aktuallen
Beispiel
wurde
UINT
CMenu::EnableMenuItem(UINT nIDEnableItem, UINT nEnable) verwendet.
Wichtige Parameter zu UINT nIDEnableItem:
SC_CLOSE
SC_MAXIMIZE
SC_MINIMIZE
"X"
Maximiert das Fenster
Minimiert das Fenster
417
Die Programmiersprache C++
Wichtige Parameter zu UINT nEnable:
MF_BYCOMMAND
MF_BYPOSITION
MF_DISABLED
MF_ENABLED
MF_GRAYED
zeigt an, daß der erste Parameter durch seine ID angezeigt wird
zeigt an, daß der erste Parameter durch seine Position angezeigt wird
Deaktiviert den Menüpunkt
Aktiviert den Menüpunkt
Deaktiviert den Menüpunkt (Farbe des Strings: hellgrau)
6. Entfernen der Minimize- und Maximize-Box über die Window-Styles in PreCreateWindow(...)
cs.style &= ~WS_MINIMIZEBOX;
cs.style &= ~WS_MAXIMIZEBOX;
Damit werden die Menüeinträge "Minimieren" und "Maximieren" auf hellgrau und inaktiv gesetzt.
2. Symbolleiste
Symbole der Symbolleiste stellen z.B. Werkzeuge und Hilfsmittel dar (Papier,
Ordner, Diskette, Schere, Drucker, etc.).
Das dritte Symbol "Diskette" steht bspw. für Speichern. Bilder, Icons und Symbole
sind Ressourcen. Die Standard-Symbolleiste wird über die Ressource
IDR_MAINFRAME angesprochen) unter dem Ordner Toolbar in der RessourcenAnsicht).
418
Die Programmiersprache C++
3. Statusleiste
In der vorliegenden Anwendung wird automatisch eine Statusleiste erzeugt. Die
dazugehörigen Bausteine enthält die Klasse CMainFrame:
CStatusBar
m_wndStatusBar;
static UINT indicators[] =
{
ID_SEPARATOR,
ID_INDICATOR_CAPS,
ID_INDICATOR_NUM,
ID_INDICATOR_SCRL,
};
// Statusleistenanzeige
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Statusleiste konnte nicht erstellt werden\n");
return -1;
// Fehler bei Erstellung
}
Das Objekt "Statusleiste" ist eine Instanz der Klasse CStatusBar. In der Klasse CMainFrame wird
dieses Objekt als Member-Variable definiert. In der Datei MainFrm.cpp befindet sich das globale
statische UINT-Array indicators. In der Klasse CMainFrame::OnCreate(...) wird die Status-Leiste
mit CStatusBar::Create(...), und die Funktion CStatusBar::SetIndicators(...) ordnet das Array
indicators der Statusleiste zu.
Einen weiterer Beitrag zur Statusleiste befindet sich unten im Hauptfenster:
419
Die Programmiersprache C++
Dort befindet sich der String "Bereit". Dieser Text befindet sich in der String Table unter der
Bezeichnung AFX_IDS_IDLEMESSAGE. Unmittelbar danach befinden sich in der String Table die
Indikatorbereiche. Die Reihenfolge der Indikatorbereiche stimmt mit der Reihenfolge der Definition im
Array überein. Für die Ausgabe der Befehlsinformation ist der Eintrag ID_SEPERATOR zuständig.
Jede Statusleiste besteht aus sog. "panes". Das sind rechteckige Bereiche, in denen Man
Informationen wie z.B. Texte ausgeben kann. Die Zählung der "panes" beginnt links und startet bei 0.
Der Text "Bereit" steht also in "pane" 0.
4. Das Anwendungssymbol
Unter Windows verfügt jedes Programm über ein Symbol (Icon) zur grafischen
Präsentation. Windows verwendet dieses Symbol in verschiedenen Kontexten (Titel
des Hauptfensters, Anzeige im Explorer, Task-Leiste).
Aufgabe jeder Windows-Anwendung ist es daher, ein entsprechendes Symbol
bereitzustellen.
5. Initialisierung der Anwendung
Die Funktion InitInstance() startet das SDI-Programm. Dort befindet sich eine
Sammlung diverser Vorgänge:
6.5.4.3 Bearbeitung von Kommandozeilenargumenten
Die
MFC
sieht
für
bestimmte
Kommandozeilen-Argumente
eine
Standardverarbeitung vor, die mit wenigen Methodenaufrufen implementiert werden
kann. Die von der MFC standardmäßig unterstützten Kommandozeilenargumente
werden automatisch in der ParseParam()-Methode der CCommandLineInfoKlasse ausgewertet und können über einen Aufruf der CWinApp-Methode
ProcessShellCommand() ihrer Verarbeitung zugeführt werden.
Zum Einlesen und zur Abfrage der Kommandozeilenargumente sind in der MFC die
Klasse CCommandLineInfo und die Methode ParseCommandLine() vorgesehen.
Letztere ruft zur Verarbeitung der Kommandozeile die CCommandLineInfoMethode
ParseParam()
auf,
die
man
zur
Verarbeitung
eigener
Kommandozeilenargumente überschreiben kann.
Bsp.: Unterstützung Kommandozeilenargumente. Im folgenden Programm soll das
Hauptfenster je nach Kommandozeilenargument in normaler Größe (kein Argument),
minimiert (-min) oder maximiert (-max) angezeigt werden.
1. Anlegen einer neuen SDI_Anwendung mit Hilfe des anwendungsassistenten und Doc/ViewUnterstützung.
420
Die Programmiersprache C++
Zum Überschreiben des CCommandLineInfo-Methode ParseParam() ist eine eigene Klasse
von CCommandLineInfo abzuleiten. Der Einfachheit halber wird dafür keine eigene Datei
angelegt, sondern die Dateien des Anwendungsgerüsts herangezogen.
2. Öffnen der Header-Datei, in der die Anwendungsklasse deklariert ist
3. Deklaration der abgeleiteten Klasse über die CCommandLineInfo-Klasse, in der die Methode
ParseParam() deklariert ist.
// Pr64330.h : Haupt-Header-Datei für die Anwendung PR64330
// Meine_CCommandLineInfo
class Meine_CCommandLineInfo:public CCommandLineInfo
{
public: virtual void ParseParam(const char* pszParam,
BOOL bFlag, BOOL bLast);
};
4. Öffnen der zugehörigen Quelltextdatei, implementiere hier die überschriebene Methode.
/////////////////////////////////////////////////////////////////////////
//
void Meine_CCommandLineInfo::ParseParam(const char* pszParam, BOOL bFlag,
BOOL bLast)
{
// Datenelement des Anwendungsobjekts setzen
if (lstrcmp(pszParam,"min") == 0)
AfxGetApp()->m_nCmdShow = SW_SHOWMINIMIZED;
else if (lstrcmp(pszParam,"max") == 0)
AfxGetApp()->m_nCmdShow = SW_SHOWMAXIMIZED;
CCommandLineInfo::ParseParam(pszParam,bFlag,bLast);
}
5. Scrollen in der Datei nach unten, bis in der InitInstance()-Methode die Zeile mit der
Anweisung „CCommandLineInfo cmdInfo;“ erreicht ist. „CCommandLineInfo“ ist durch
„Meine_CCommandLineInfo“ zu ersetzen.
6. Scrollen bis zum Ende der InitInstance()-Methode. Hier gibt es den Aufruf der Methode
ShowWindow(), der standardmäßig das Argument SW_SHOW übergeben wird. Ersetze das
Argument SW_SHOW durch die Elementvariable m_nCmdShow.
7. Ausführen des Programms. Test der verschiedenen Kommandozeilenargumente.
Zur
Übergabe
von
Kommandozeilenargumente
im
Debugger
wechsle
über
PROJEKT/EINSTELLUNGEN zum Dialogfenster PROJEKTEINSTELLUNGEN und gib
Kommandozeilenargumente in das Eingabefeld PROGRAMMARGUMENTE ein.
421
Die Programmiersprache C++
Angabe von Kommandozeileanargumenten über den Debugger
6.5.4.4 Die Nachricht WM_PAINT
Die WM_PAINT-Nachricht wird von Windows an Fenster geschickt, um mitzuteilen,
dass sich das Fenster neu zeichnen soll. Das ist bspw. der Fall, wenn ein Fenster
vom Anwender aus dem Hintergrund wieder in der Vordergrund gehoben wird.
Windows kann zwar den Fensterrahmen rekonstruieren, nicht aber den im ClientBereich des Fensters angezeigten Text oder etwaige Grafiken. Es schickt daher eine
WM_PAINT-Nachricht an das betreffende Fenster. Aufgabe des Programmierers ist
es, die Nachricht abzufangen und mit der Funktion zu verbinden, die den
Fensterinhalt rekonstruiert:
-
in MFC-Anwendungen ohne Doc/View-Unterstützung behandelt man die WM_PAINT-Nachricht in
einer OnPaint()-Behandlungsmethode.
in MFC-Anwendungen mit Doc/View wird die Nachricht von der Ansichtsklasse der MFC
automatisch abgefangen und in der Methode OnDraw()186 behandelt. Diese Methode muß in der
abgeleiteten Klasse überschrieben werden. Der Anwendungsassistent tut dies automatisch beim
Anlegen des Anwendungsgerüsts.
a) Ausserhalb von OnDraw() zeichnen.
Bsp.: Zeichnen von einem Kreis auf einen Mausklick
1. Anlegen eines neuen Projekts (SDI-Anwendung mit Doc/View-Unterstüzung)
186
Intern wird in den Ansichtsklassen des Doc/View-Gerüsts die WM_PAINT-Nachricht von einer OnPaint()Methode abgefangen und bearbeitet. In dieser OnPaint()-Methode werden einige für das Zeichnen benötigte
Vorarbeiten erledigt, dann wird die Methode OnDraw() aufgerufen, in die der Code eingefügt wird.
422
Die Programmiersprache C++
2. Aufruf des Klassen-Assistenten, Einrichten einer Behandlungsroutine für die
WM_LBUTTONDOWN-Nachricht.
3. Einsetzen des Quellcodes in die Behandlungsroutine OnLButtonDown()
// Cpr65440View Meldungshandler
void Cpr65440View::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein,
// und/oder benutzen Sie den Standard.
CClientDC dc(this); // Besorgen Geraetekontext
dc.Ellipse(point.x-20,point.y-20,point.x+20,point.y+20);
CView::OnLButtonDown(nFlags, point);
}
4. Ausführen des Programms
b) Innerhalb von OnDraw() zeichnen.
Bsp.: Zeichnen eines Kreises
1. Anlegen eines neuen Projekts mit dem MFC-Anwendungs-Assistenten (SDI-Anwendung mit
Doc/View-Unterstützung)
2. Für die Ansichtsklasse wird die WM_PAINT Nachricht bereits vom Anwendungsgerüst abgefangen
und mit dem Aufruf der Methode OnDraw() verbunden (Expansion des Knotens Ansichtsklasse,
Doppelklick auf Eintrag OnDraw())
3. Einsetzen des Quellcodes für die Behandlungsmethode OnDraw()
// Cpr65442View-Zeichnung
void Cpr65442View::OnDraw(CDC* pDC)
{
Cpr65442Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
pDC->Ellipse(50,50,70,70);
}
4. Ausführen des Programms
Da hinter dem Doc/View-Modell die Trennung von Datenverwaltung und
Datenanzeige steht, ist die Ansichtsklasse üblicherweise gezwungen, sich die
anzuzeigenden Daten vom zugehörigen Dokument zu besorgen (Zeiger pDoc mit
Zugriff auf public-Methoden)
Bsp.: Kreise an Mausklickposition zeichnen
1. Anlegen einer neuen SDI-Anwendung mit Unterstützung für Doc/View durch den MFCAnwendungsassistenten
2. Deklarieren eines int-Zählers m_nZaehler in der Dokumenten-Klasse und eines CPoint-Array für
100 Kreismittelpunkte
class Cpr65442Doc : public CDocument
{
protected: // Nur aus Serialisierung erstellen
Cpr65442Doc();
DECLARE_DYNCREATE(Cpr65442Doc)
// Überschreibungen
public:
virtual BOOL OnNewDocument();
423
Die Programmiersprache C++
virtual void Serialize(CArchive& ar);
// Implementierung
public:
virtual ~Cpr65442Doc();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
// Generierte Funktionen für die Meldungstabellen
protected:
DECLARE_MESSAGE_MAP()
public:
int m_nZaehler;
CPoint m_Kreise[100];
};
3. Initialisieren der Elementvariablen m_nZaehler im Konstruktor der Dokumentenklasse mit dem
Wert 0.
// Cpr65442Doc Erstellung/Zerstörung
Cpr65442Doc::Cpr65442Doc()
: m_nZaehler(0)
{
// TODO: Hier Code für One-Time-Konstruktion einfügen
}
4. Einrichten einer Nachrichtenbehandlungsmethode für WM_LBUTTONDOWN in der Klasse des
Ansichtsfensters mit Hilfe des Klassenassistenten zum Speichern der Kreismittelpunkte
// Cpr65442View Meldungshandler
void Cpr65442View::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein,
// und/oder benutzen Sie den Standard.
CPr64342Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CClientDC dc(this);
int b = (int) (100.0 * rand() / RAND_MAX);
dc.Ellipse(point.x-b,point.y-b,point.x+b,point.y+b);
if (pDoc->m_nZaehler < 100) pDoc->m_nZaehler++;
pDoc->m_Kreise[pDoc->m_nZaehler - 1] = point;
CView::OnLButtonDown(nFlags, point);
}
5. Rekonstruktion der Kreise mit der OnDraw()-Methode
// Cpr65442View-Zeichnung
void Cpr65442View::OnDraw(CDC* pDC)
{
Cpr65442Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
for (int i = 0;i < pDoc->m_nZaehler;i++)
{
int b = (int)(100.0 * rand() / RAND_MAX);
pDC->Ellipse(pDoc->m_Kreise[i].x - b,
pDoc->m_Kreise[i].y - b,
pDoc->m_Kreise[i].x + b,
424
Die Programmiersprache C++
pDoc->m_Kreise[i].y + b);
}
pDC->Ellipse(50,50,70,70);
}
Das Anstoßen des Neuzeichnens des Fensters kann auch durch Selbstauslösung einer WM_PAINTNachricht (Aufruf der CWnd-Methode UpdateWindow()) erfolgen. Zur Sicherheit sollte man zuvor den
Fensterinhalt als ungültig erklären.
void Cpr65442View::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein,
// und/oder benutzen Sie den Standard.
Cpr65442Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CClientDC dc(this);
int b = (int) (100.0 * rand() / RAND_MAX);
dc.Ellipse(point.x-b,point.y-b,point.x+b,point.y+b);
if (pDoc->m_nZaehler < 100) pDoc->m_nZaehler++;
pDoc->m_Kreise[pDoc->m_nZaehler - 1] = point;
Invalidate();
UpdateWindow();
CView::OnLButtonDown(nFlags, point);
}
Man kann dem Anwender auch die Möglichkeit geben, den Fensterinhalt zu löschen:
1. Laden der Menü-Ressource IDR_MAINFRAME in den Menü-Editor
2. Löschen aller Menübefehle bis auf DATEI/NEU und DATEI/BEENDEN
3. Überschreiben der Methode DeleteContents() in der Dokumentenklasse
void Cpr65442Doc::DeleteContents(void)
{
// TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
m_nZaehler = 0;
CDocument::DeleteContents();
}
6.5.4.5 Zeitgeber (WM_TIMER)
Mit Hilfe der Nachricht WM_TIMER kann eine Art Zeitgeber implementiert werden, der
ein Programm in immer gleichen Zeitabständen benachrichtigt. Ein Timer ist eine
interne Routine, die Windows dazu veranlasst in bestimmten Zeitintervallen die
Nachricht WM_TIMER zu senden. Das Zeitintervall wird durch den time_out-Wert
bestimmt. Ein time_out ist eine Zeitspanne, die vom Timer in Millisekunden
gemessen wird.
In Windows muß der Timer mit Hilfe der Member-Funktion SetTimer() gesetzt
werden. Danach muß mit Hilfe des Klassenassistenten eine Nachrichtenfunktion für
die Nachricht WM_TIMER erzeugt werden. In dieser Funktion liegt die Anweisung, die
in bestimmten Zeitintervallen wiederholt werden sollte.
425
Die Programmiersprache C++
6.6 Bilder, Zeichnungen und Bitmaps
6.6.1 Die grafische Geräteschnittstelle (GDI)
Gerätekontexte
Bevor man irgendeine Grafik erstellen kann, muß man über den Gerätekontext
verfügen, in dem die Grafiken angezeigt werden. Der Gerätekontext enthält
Informationen über das System, die Anwendung und das Fenster, in dem die
Ausgabe der Zeichnungen und Bilder erfolgt. Das Betriebssystem entnimmt dem
Gerätekontext, in welchem Kontext eine Grafik zu zeichnen ist, wie groß der
sichtbare Bereich ist, und wo sich der Zeichenbereich momentan auf dem Bildschirm
befindet.
Eine Grafikausgabe erfolgt im Kontext eines Anwendungsfensters aus. Dieses
Fenster kann jederzeit als Vollbild, minimiert, teilweise überdeckt oder vollständig
unsichtbar sein. Windows verwaltet alle Gerätekontexte und ermittelt daraus, wieviel
und welcher Teil der gezeichneten Grafiken tatsächlich für den Benutzer anzuzeigen
ist.
Die meisten Funktionen in bezug auf Zeichnungen und Bilder führt der Gerätekontext
mit zwei Ressourcen aus: Stift und Pinsel. Der Gerätekontext verwendet Stifte, um
Linien und Figuren zu zeichnen, während Pinsel die Flächen auf dem Bildschirm
ausfüllen.
Die Gerätekontextklassen
In Visual C++ stellt die MFC-Gerätekontextklasse (CDC) zahlreiche
Zeichenfunktionen für Kreise, Quadrate, Linien, Kurven usw. bereit. Die Instanz einer
Gerätekontextklasse erstellt man mit einem Zeiger auf die Fensterklasse, die man
mit dem Gerätekontext verbinden möchte. Damit läßt sich in den Konstruktoren und
Destruktoren der Gerätekontextklasse der gesamte Code unterbringen, der sich mit
der Zuweisung und Freigabe eines Gerätekontextes befaßt.
Gerätekontextobjekte gehören genau wie alle Arten von Zeichenobjekten zu den
Ressourcen im Betriebssystem Windows. Das Betriebssystem verfügt nur über eine
begrenzte Zahl dieser Ressourcen.187 Es empfiehlt sich also, die Ressourcen in
Funktionen zu erzeugen, wo sie zum Einsatz kommen, und sie sobald wie möglich
wieder freizugeben, wenn die Arbeit mit den betreffenden Ressourcen
abgeschlossen ist.
Dementsprechend nutzt man Gerätekontexte und deren Zeichenressourcen fast
ausschließlich als lokale Variablen innerhalb einer einzigen Funktion. Die einzige
echte Ausnahme liegt vor, wenn das Gerätekontextobjekt von Windows erzeugt und
in eine ereignisverarbeitende Funktion als Argument übergeben wird.
Die MFC stellt für die verschiedenen Ausgabegeräte spezielle Klassen bereit:
187
Obwohl die Gesamtzahl der Ressourcen in den neueren Versionen von Windows recht groß ist, kann dennoch
ein Mangel an Ressourcen entstehen, wenn eine Anwendung die Ressourcen zwar reserviert, aber nicht wieder
korrekt freigibt. Diesen Verlust bezeichnet man als Ressourcenlücke, die analog zu einer Speicherlücke das
System des Benutzer blockieren oder lahmlegen kann.
426
Die Programmiersprache C++
Klasse
CDC
CPaintDC
CClientDC
CWindowDC
Ausgabeeinheit
Basisklasse aller Gerätekontextklassen mit reicher Auswahl an
Zeichen- und Ausgabeoperationen.
Spezieller
Fenster-DC
zur
Implementierung
von
Behandlungsmethoden zur WM_PAINT-Nachricht.
Die Fensteklassen der MFC implementieren standardmäßig bereits
OnPaint()-Methoden zur Behandlung der WM_PAINT-Nachricht. In
dieser wird eine CPaintDC-Instanz gebildet und an die virtuelle
Methode OnDraw() übergeben, die zur Ausgabe überschrieben
werden soll.
Gerätekontext für die Ausgabe in den Client-Bereich eines Fensters
Gerätekontext für den gesamten Bereichs eines Fensters (einschl.
Rahmen, Titel).
Abb.: Gerätekontextklassen für Fenster
Windows speichert keine Informationen über Fensterinhalte. Das bedeutet: Bei
bestimmten Fensteroperationen (z.B. Verkleinern, Vergrößern, Minimieren, in den
Vordergrund holen) kann der Fensterinhalt nicht in Windows rekonstruiert werden.
Windows benutzt in solchen Fällen die Nachricht WM_PAINT zur Information an das
Fenster, seine Inhalte selbst zu zeichnen. Das MFC-Anwendungsgerüst fängt diese
Nachricht ab und ruft die virtuelle Methode OnDraw() auf. In OnDraw() gehören
alle Anweisungen zur Rekonstruktion (üblicherweise der gesamte Text und
Grafikausgabe) des Fensters.
-
Für Zeichenausgabe in OnDraw() übergibt die MFC einen passenden Gerätekontext als Argument
an OnDraw().
Für Zeichenausgaben an andere Methoden muß ein Gerätekontext als Instanz einer passenden
Gerätekontextklasse erzeugt werden.
Die Zeichenwerkzeuge
CBitMap
CBrush
CFont
CPalette
CPen
CRgn
für Pinsel-Objekte
für Schriftarten
für Paletten (von 256 Farben)
für Stiftobjekte
für Zeichenbereiche
Jeder Gerätekontext ist standardmäßig mit einem Satz vordefinierter GDI-Objekte
ausgestattet. Falls bspw. ohne Laden eines Pinsels oder Stfts in den Gerätekontext
(eine Linie oder ein Rechteck) gezeichnet wird, dann wird für Linie und Rahmen des
Rechtecks das Standard-Stiftobjekt verwendet, das dünne schwarze Linien zieht.
Ausgemalt wird das Rechteck mit dem Standardpinsel, der weiß und ohne Muster
ist. Anderenfalls muß man CDC-Methoden mit Farben und Pinsel als Argumente
übernehmen, die GDI-Objekte des Gerätekontexts ersetzen.
Die Zeichenmethoden
Die eigentlichen Zeichenmethoden werden mit Hilfe der Methoden des
Gerätekontexts ausgeführt. In der Basisklasse für die Gerätekontexte (CDC) sind
zahlreiche Methoden zum Zeichnen definiert. Diese Methoden können auch in den
abgeleiteten Gerätekontextklassen (CPaintDC und CClientDC) verwendet werden.
427
Die Programmiersprache C++
Methode
Linien
CPoint MoveTo(int x, int y);
CPoint MoveTo(POINT point);
Ausgabeeinheit
Die Linienfunktionen nutzen das Konzept der
„aktuellen Zeichenposition“. Mit dieser Methode
kann die aktuelle Zeichenposition auf eine
bestimmte Koordinate im Gerätekontext gesetzt
werden.
Die Methode legt den Anfangspunkt einer Linie
fest und liefert die letzte aktuelle Zeichenposition
zurück.
Zeichnet
eine
Linie
von
der
aktuellen
zeichenposition bis zu der als Argument
übergebenen Koordinate.
Die übergebene Koordinate ist danach die neue
aktuelle Zeichenposition.
BOOL LineTo(int x, int y);
BOOL LineTo(POINT point);
Bögen
BOOL Arc(int x1, int y1, int x2, int y2, int x3,
int y3, int x4, int y4);
BOOL Arc(lpRect, Point ptStart, POINT ptEnd);
Rechtecke
void Draw3dRect(LPCRect lpRect, COLORREF
clrTopLeft, COLORREF clrBottomRight);
void Draw3dRect(int x, int y, int cx, int cy,
COLORREF clrTopLeft, COLORREF
clrBottomRight);
void FillRect(LPCRECT lpRect, CBrush* pBrush);
Zeichnet einen elliptischen Bogen. Der Bogen wird
spezifiziert durch ein umgebendes Rechteck,
sowie einen Anfangs- und Endpunkt (die nicht
unbedingt auf dem Bogen liegen müssen)
Position und Abmessung des zu zeichnenden
Rechtecks werden durch Angabe der oberen
linken Ecke sowie Breite und Höhe oder durch
Übergabe eines CRect- oder Rect-Objekts
spezifiziert.
Zeichnet
ein
dreidimensionales
Rechteck.
Übergeben wird ein umgebendes Rechteck, die
Farbe für den oberen und linken Teil des
Rahmens und die Farbe für den unteren und
rechten Teil des Rahmens
Zeichnet ein Rechteck und füllt es in Farbe und
dem Muster des übergebenen Pinsel-Objekts. Der
linke und obere Rahmen werden ebenfalls
eingefärbt.
Zeichnet ein Rechteck und füllt es in der
angegebenen Farbe
void FillSolidRect(LPCRECT lpRect,
COLORREF clr);
void FillSolidRect(int x, int y, int cx, int cy,
COLORREF clr);
Zeichnet eine Rahmen und das angegebene
void FrameRect(LPCRECT lpRect, CBrush*
pBrush); Rechteck. Das Pinselobjekt bestimmt farbe und
Muster des Rahmens.
Zeichnet ein Rechteck. Der Rahmen wird mit dem
BOOL Rectangle(int x1, int y1, int x2, int y2);
Stift-Objekt, der Inhalt des Rechtecks mit dem
Pinsel-Objekt des Gerätekontexts gezeichnet.
BOOL Rectangle(LPRECT lpRect);
Zeichnet ein Rechteck mit abgerundeten Ecken.
BOOL RoundRect(int x1, int y1, int x2, int y2,
int x3, int y3); Die Abrundung wird durch Breite und Höhe einer
Ellipse definiert.
BOOL RoundRect(LPRECT lpRect, POINT
point);
Kreise
Zeichnet ein Ellipsensegmet (Schnittfigur einer
BOOL Chord(int x1, int y1, int x2, int y2, int x3,
int y3, int x4, int y4); Ellipse mit einer Linie). Übergeben werden das
umschließendeRechteck
sowie
Startund
BOOL Chord(LPCRECT lpRect, POINT ptStart,
endpunkt
der
schneidenden
Linie
POINT ptEnd);
Zeichnet eine Ellipse, die das übergebene
BOOL Ellipse(int x1, int y1, int x2, int y2);
Rechteck ausfüllt. Ist das Rechteck ein Quadrat,
BOOL Ellipse(LPRect lpRect);
dann entsteht ein Kreis.
Zeichnet ein Tortenstück aus einer Ellipse.
BOOL Pie(int x1, int y1, int x2, int y2, int x3,
int y3, int x4, int y4); Übergeben werden das umschließende Rechteck
der Ellipse sowie Start- und Enpunkt des Bogens.
BOOL Pie(LPRECT lpRect, POINT ptStart,
POINT ptEnd);
Polygone
428
Die Programmiersprache C++
BOOL Polygon(LPPoint lpPoints, int nCount);
Zeichnet ein Polygon. Übergeben werden die
Punkte, die zu verbinden sind. Der letzte Punkt
wird automatisch mit dem ersten Punkt verbunden
Abb.: Auswahl an Zeichenmethoden
Das RGB-Modell
Die meisten Methoden, denen man Farbe übergeben kann, erwarten einen
COLORREF-Wert, z.B. der Konstruktor für das Pinselobjekt CBrush(COLORREF
crColor). Hinter COLORREF verbirgt sich ein 32-Bit-Wert, der eine Farbe nach dem
RGB-Modell spezifiziert. In einem COLORREF-Wert kodiert das unterste Byte den
Rotanteil, das zweite Byte den Grünanteil und das dritte Byte den Blauanteil.
429
Die Programmiersprache C++
6.7 Die Sammlungsklassen der MFC
Es gibt vorlagenbasierte (Template Based) und nicht-volagenbasierte188
Sammlungsklassen. Die Sammlungsklassen bestehen aus Listenklassen (Lists),
Datenfeldklassen (Arrays) und Zuordnungsklassen (Maps. Dictionaries).
Vorlagenbasierte Sammlungsklassen können nach Einfügen von #include
<afxtempl.h> in die Datei stdAfx.h benutzt werden.
Die vorlagenbasierte Sammlungsklasse CArray
template< class TYPE, class ARG_TYPE > class CArray : public CObject
TYPE ist der von CObject abgeleitete Typ des Objektes, der in CArray gespeichert wird. ARG_TYPE
ist der Typ, mit dem auf die Objekte zugegriffen wird. Ein praktisches Beispiel kann wie folgt aussehen:
CArray<Student, Student&> StudArray;
Student studi;
StudArray->Add( studi );
Im obigen Beispiel werden Objekte des Typs Student in CArray gespeichert. Der
Zugriffstyp ist eine Referenz auf die Klasse Student selbst. Add() ist eine MemberFunktion der Klasse CArray, mit deren Hilfe ein neues Objekt in die
Sammlungsklasse gespeichert wird.
Die vorlagenbasierte Sammlungsklasse CTypedPtrArray
template<class BASE_CLASS, class TYPE>
class CTypedPtrArray : public BASE_CLASS
Der erste Parameter BASE_CLASS ist die Basiskasse für das Feld von Zeigern. Dies muß eine
Datenfeldklasse sein. Da Sie diese Klasse bestimmt zum Serialisieren benutzen, kommt als
Basisklasse für CTypedPtrArray nur die Datenfeldklasse CObArray in Frage. TYPE kennzeichnet
den Typ der Elemente, die in CObArray gespeichert werden. Ein praktisches Beispiel könnte
folgendermaßen aussehen:
CTypedPtrArray<CObArray, Student*> studi;
for(int i=0; i<studi.GetSize(); i++)
{ Student* st= studi.GetAt(i);}
Mit der Member-Funktion GetSize() wird die Anzahl der Elemente ermittelt und mit
der Member-Funktion GetAt() werden die Elemente gelesen. Ein gewichtiges
Argument für die Benutzung dieser Klasse, oder der Klasse CTypedPtrList ist
darin begründet, daß diese beiden Klassen typsicher sind.
Bsp.: Objekte der im vorhergehenden Abschnitt angegebenen eigenen Klasse
Student sollen in der vorlagenbasierten MFC-Klasse CArray gelagert werden. Es ist
praktisch am Ende der Header-Datei der eigenen Klasse Student Student.h zu
diesem Zweck, die folgende Anweisung anzugeben:
typedef CArray <Student*, Student*> StudentList
StudentList ist der neue Typ der Daten, der in einer SDI-Anwendung direkt für die
Trennung von Dokument und Ansicht benutzt werden kann. Damit die Anwendung
188
Stammen aus früheren Versionen der MFC-Bibliothek
430
Die Programmiersprache C++
der vorlagenbasierten MFC-Sammlungsklasse erkennen kann, muß in stdAfx.h
die Anweisung #include <afxtempl.h> stehen.
In die Header-Datei der Ansicht können dann folgende Deklarationen aufgenommen
werden:
// Attribute
public:
CPr64420Doc* GetDocument();
Student* st; // fuer den Zugriff auf Member-Funktionen von Student
int c; // Laufvariabble zur Navigation zwischen Datensaetzen
BOOL info;
BOOL loesch; // noetig zur Befehlsaktuaktualisierung
BOOL naechst; // noetig zur Befehlsaktualisierung
// Operationen
public:
void leseStudent();
// liest Eingaben und transportiert sie ins Dokument
void zeigeStudent(Student *);
// Ausgabe der Daten eines Student-Objekts auf den Bildschirm
Die Implementierungen von leseStudent() und zeigeStudent() sind:
void CPr64420View::leseStudent()
{
CPr64420Doc* pDoc = GetDocument();
if(st=!NULL)
{
UpdateData(TRUE);
st=new Student;
st->setName(m_name);
st->setMatrik(m_matrik);
st->setGruppe(m_gruppe);
pDoc->insertStudent(st);
m_name
= "";
m_matrik = "";
m_gruppe = "";
UpdateData(FALSE);
(((CEdit*)GetDlgItem(IDC_NAME)->SetFocus()));
}
}
void CPr64420View::zeigeStudent(Student* aktStudent)
{
m_name
= aktStudent->getName();
m_matrik = aktStudent->getMatrik();
m_gruppe = aktStudent->getGruppe();
UpdateData(FALSE);
}
Die Header-Datei des Dokuments kann folgende Deklaration aufnehmen:
// Attribute
public:
int anzahl; // Index des Datenfelds
StudentList* starray; // Zeiger auf (den Typ der) Datensaetze
// Operationen
public:
// Einfuegen eines Datensatzes in die Auflistung StudentList
void insertStudent(Student*);
// Ansprache der Datensaetze in der Auflistung
StudentList* getList() { return starray; }
431
Die Programmiersprache C++
Die Implementierung im der Funktion insertStudent() sieht im Dokument
folgendermaßen aus:
void CPr64420Doc::insertStudent(Student* s)
{
starray->Add(s);
SetModifiedFlag();
}
Die Serialisierung der Datensätze findet in der Funktion Serialize() des
Dokuments statt:
void CPr64420Doc::Serialize(CArchive& ar)
{
anzahl = 0;
if (ar.IsStoring())
{
// ZU ERLEDIGEN: Hier Code zum Speichern einfügen
anzahl = starray->GetSize();
//Anzahl der Datensätze.
ar << anzahl;
for(int i=0;i<starray->GetSize();i++)
{
ar << starray->GetAt(i);
}
}
else
{
// ZU ERLEDIGEN: Hier Code zum Laden einfügen
Student* s;
ar >> anzahl;
for(int i = 0;i < anzahl; i++)
{
ar >> s;
starray->Add(s);
}
}
}
432
Die Programmiersprache C++
6.6.2 Die Zeichenwerkzeuge
GDI-Objekte
Nachdem ein Gerätekontext vorliegt, wird ein Zeichenwerkzeug benötigt, z.B. ein
Zeichenstift, ein Pinsel oder auch eine Schriftart für eine Textausgabe. Diese
Zeichenwerkzeuge werden GDI-Objekte genannt. Jedem GDI-Objekt entspricht eine
eigene Klasse, z.B. CPen, CBrush. GDI-Objekte werden bei der Instanzenbildung
konfiguriert und danach in den Gerätekontext geladen, wobei von jeder Art GDIObjekt genau eine Instanz in einem Gerätekontext vorhanden ist.
Wird in den Gerätekontext gezeichnet, dann wird, je nachdem welche
Zeichenoperation (CDC-Methoden) aufgerufen werden, automatisch das
entsprechende Zeichenwerkzeug benutzt.
Die Stiftklasse CPen für Stiftobjekte
Die Pinselklasse CBrush für Pinselobjekte.
Die Klasse CFont für Schriftarten. Das aktuelle Font-Objekt des Gerätekontextes
wird für Textausgaben (CDC-Methode TextOut()) benutzt.
Die Klasse CPalette für Paletten mit 256 Farben.
Die Klasse CRgn für Zeichenbereiche.
Jeder Gerätekontext ist standardmäßig mit einem Satz vordefinierter GDI-Objekte
ausgestattet (sog. „Stock-Objects“). Beim Zeichnen einer Linie oder eines Rechtecks
wird (ohne spezifisch geladenen Stift oder Pinsel) ein Standard-Stiftobjekt
verwendet, das dünne schwarze Linien zieht. Ausgemalt wird das Rechteck mit dem
Standardpinsel, der weiß und ohne Muster ist. Falls diese Vorgaben nicht zusagen,
dann müssen
-
CDC-Methoden verwendet werden, die Farben oder Pinsel-Objekte als Argumente übernehmen
die GDI-Objekte des Gerätekontexts ersetzt werden.
Die Klasse CBitmap
Es gibt verschiedene Möglichkeiten, um Bilder in einer Anwendung anzuzeigen.
Feststehende Bitmaps nimmt man als Ressourcen mit zugewiesenen Objekt-IDs in
die Anwendung auf und zeigt sie mit statischen Bildsteuerelementen oder ActiveXSteuerelementen an. Man kann auch die Bitmap-Klasse CBitmap einsetzen, um die
Anzeige der Bilder weitgehend beeinflussen zu können. Mit Hilfe der Bitmap-Klasse
lassen sich Bitmaps dynamisch aus Dateien des Systemlaufwerks laden, die Bilder
bei Bedarf in der Größe ändern und sie an den zugewiesenen Platz anpassen.
Einrichten von GDI-Objekten
Zum Laden eines GDI-Objekts in einen Gerätekontext, verwendet man die CDCMethode SelectObject():
CBrush roterPinsel(RGB(255,0,0));
pDC->SelectObject(&roterPinsel);
Beim Laden eines GDI-Objekts in einen Gerätekontext (z.B. in ein Cpen-Objekt) wird
das alte Stiftobjekt, das zuvor im Gerätekontext war, aus dem Gerätekontext
entfernt. Handelt es sich um ein von Windows vordefiniertes Objekt, dann braucht
433
Die Programmiersprache C++
man sich um das Löschen dieses Objekts nicht zu kümmern. Handelt es sich um ein
selbst definiertes Objekt, dann sollte es gelöscht werden. Das geschieht
-
entweder automatisch, wenn der Gerätekontext selbst aufgelöst wird
oder indem man ein neues GDI-Objekt lädt, das alte GDI-Objekt überschreibt und damit löscht.
Das RGB-Modell
Die meisten Methoden, denen man Farben übergeben kann, erwarten einen
COLORREF-Wert, bspw. auch der Konstruktor für das Pinselobjekt CBrush:
CBrush(COLORREF crColor).
Hinter COLORREF verbirgt sich ein 32 Bit-Wert, der eine Farbe nach dem RGBModell189 spezifiziert. In einem COLORREF-Wert kodiert das unterste Byte den
Rotanteil, das zweite Byte den Grünanteil und das dritte Byte den Rotanteil.
Abbildungsmodi und Koordinatensysteme
Die Wahl der Koordinaten ist nicht immer beliebig. Es gibt eine Reihe von
Funktionen, die jeweils nur mit logischen oder Gerätekoordinaten 190 arbeiten. Die
MFC stellt Funktionen zur Umwandlung von Koordinaten bereit 191.
Abbildungdmodi legen die Skalierung für die Größe der Grafik fest. Der
voreingestellte Wert für die Koordinaten ist MM_TEXT. Hier entspricht die Einheit
des Koordinatensystems einer Pixelgröße192.
189
Beruht auf dem Effekt, dass man durch Variation aus den drei Lichtfarben Rot, Grün und Blau sämtliche
Farben mischen kann.
190 CDC-Menmber-Funktionen benutzen logische Koordinaten, CWnd-Funktionen benützen Gerätekoordinaten
191 in Gerätekontextklassen und der Klasse CWnd
192 Je nach Auflösung des Bildschirms und der Grafikkarte kann sich die Größe eines Pixels ändern.
434
Die Programmiersprache C++
7. C# und .NET
Bedeutung von C# für Visual Studio .NET
C# („ßi scharp“) ist eine Neuentwicklung von Micosoft, ein Konkurrent zu Java und
im Gegensatz zu C++ keine Erweiterung von C. C# wurde in .NET eingebettet und
besitzt keine Standardbibliotheken. .NET enthält Klassenbibliotheken, die
Programmierern in Visual Basic, JScript und Visual C++ bekannt vorkommen. C#
kann als Mischung aus Java, C++ und Visual Basic bezeichnet werden. C# gehört zu
den .NET-Programmiersprachen und ist objektorientiert. C# spielt im Visual Studio
keine Sonderrolle. C# ist aber die erste Microsoft-Programmiersprache, die sich an
dem ECMA-Standard hält. In der Praxis bedeutet dies, dass Fremdfirmen auf der
Basis dieses Standards spezielle Tools und auch eigene Compiler für C# entwickeln
können. Auch große Teile des .NET Frameworks sind in C# geschrieben. Daher
kann wohl behauptet werden, dass C# zumimdest künftig die wichtigste der .NETProgrammiersprachen ist.
Erstellen eines C#-Projekts
Visual Studio ordnet den Quellcode in Projektmappen (Ordner, in denen ein oder
mehrere Projekte gespeichert werden) an. Einstellungen der Projektmappe werden
in Dateien mit der Endung .sln gespeichert. Beim Erzeugen eines neuen Projekts
wird automatisch eine neue Projektmappe erstellt.
1. Schritt: Erzeugen eines neuen Projets "Quicksort-Anwendung".
1. Datei | Neu | Projekt
2. Setze C#-Projekte auf der rechten Seite und Konsolenanwendung auf der
linken Seite
3. Spezifiziere unter Name: pr71200 und unter Speicherort das Verzeichnis, in dem
das Projekt abgelegt werden soll.
Das Projet wird nach einem Klick auf OK erzeugt und umfasst zwei Dateien:
assemblyinfo.cs
Class1.cs
435
Die Programmiersprache C++
Visual Studio .NET hat eine Lösung für ein einzelnes C# Sharp Projekt erzeugt. Es
erscheint folgende Darstellung:
2. Schritt: Sündenfall "Hallo Welt".
Quellcodemodifikation der vordefinierten Schablone Class1.cs
436
Die Programmiersprache C++
using System;
namespace pr71200
{
/// <summary>
/// Zusammendfassende Beschreibung für Class1.
/// </summary>
class Class1
{
/// <summary>
/// Der Haupteinstiegspunkt für die Anwendung.
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: Fügen Sie hier Code hinzu, um die Anwendung zu starten
Console.WriteLine("Hallo, C# .NET Welt!");
}
}
}
Kompilieren
1. Auswahl pr72100 erstellen im Menü Erstellen.
2. Fehler und Nachrichten vom C# Compiler werden im Ausgabefenster ausgegeben. Falls keine
Fehler vorliegen, kann die Anwendung gestartet werden nach Klick auf "Starten ohne Debuggen"
im Menü Debuggen.
Programm-Ausgabe
Die Funktion WriteLine() der Klasse Console gibt den String "Hallo, C# .NET Welt!" aus,
anschließend folgt ein Zeilenvorschub. Die Funktion kann auch Werte anderer Datentypen (z.B. ganze
Zahlen, Gleitpunktzahlen) ausgeben. Die Main()-Funktion übernimmt die Steuerung, nachdem das
Programm geladen wurde.
3. Schritt: Prgrammstruktur
Die using-Direktive
437
Die Programmiersprache C++
Im .NET Framework sind einige nützliche Klassen für den Entwickler vorhanden. So übernimmt die
Klasse Console die Ein-, Ausgabe des Konsolen-Fensters. Die Klassen sind hierarchisch organisiert.
Der voll qualifizierte Name der Klasse Console ist System.Console. Andere Klassen sind bspw.
System.IO.FileStream und System.Collections.Queue. Die Direktive using erlaubt eine
Referenz auf die Klassen im zugehörigen Namensraum ohne voll qualifizierte Namensnennung.
Klassen-Deklaration
Jede Funktion ist in C# Bestandteil einer Klasse. Eine neue Klasse wird über die class-Anweisung
deklariert. In der Klasse Class1 gibt es nur eine einzige Funktion Main(). Sie empfängt die Steuerung,
wenn das Programm geladen wird. Es ist möglich über Main() Kommandozeilen-Argumente zu
übergeben.
4. Schritt: Ein- und Ausgabe über die Console
Quellcodemodifikationen
using System;
namespace pr71200
{
/// <summary>
/// Zusammendfassende Beschreibung für Class1.
/// </summary>
class Class1
{
/// <summary>
/// Der Haupteinstiegspunkt für die Anwendung.
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: Fügen Sie hier Code hinzu, um die Anwendung zu starten
// Beschreibung der Programm-Funktion
Console.WriteLine("Quicksort c# Beispiel-Applikation\n");
// Name der Eingabedatei ?
Console.Write("Quelle: ");
string szSrcFile = Console.ReadLine();
// Name der Ausgabedatei ?
Console.Write("Ausgabe: ");
string destFile = Console.ReadLine();
}
}
}
Lesen von der Konsole
Die Methode ReadLine() der Klasse Console verlangt Eingabe vom Benutzer und erfasst die
eingebene Zeichenkette. Automatisch wird die Speicherzuweisung für die Zeichenkette veranlasst. der
.NET Garbage Collector besorgt die Freigabe von belegtem Speicher.
Programm-Ausgabe
Mit Debuggen | Start ohne Debuggen ergibt sich das folgende Konsolen-Fenster:
438
Die Programmiersprache C++
5. Schritt: Verwendung eines Array zur Zwischenspeicherung eingelesener Objekte
Die Eingabe wird in der .NET Basisklasse ArrayList aufgenommen, die ein "Array
of Objects" implementiert.
Quellcodemodifikationen
using System;
using System.Collections;
using System.IO;
namespace pr71200
{
/// <summary>
/// Zusammendfassende Beschreibung für Class1.
/// </summary>
// Deklaration der Anwendungsklasse
class Class1
{
/// <summary>
/// Der Haupteinstiegspunkt für die Anwendung.
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: Fügen Sie hier Code hinzu, um die Anwendung zu starten
// Beschreibung der Programm-Funktion
Console.WriteLine("Quicksort c# Beispiel-Applikation\n");
// Name der Eingabedatei ?
Console.Write("Quelle: ");
string szSrcFile = Console.ReadLine();
// Name der Ausgabedatei ?
Console.Write("Ausgabe: ");
string szDestFile = Console.ReadLine();
// TODO: Einlesen der Quelle
string szSrcLine;
ArrayList szContents = new ArrayList();
FileStream fsInput = new FileStream(szSrcFile,
FileMode.Open,
FileAccess.Read);
StreamReader szInput = new StreamReader(fsInput);
while ((szSrcLine = szInput.ReadLine()) != null)
439
Die Programmiersprache C++
{
// Anhaengen an Array
szContents.Add(szSrcLine);
}
szInput.Close();
fsInput.Close();
// TODO: Quicksort
FileStream fsOutput = new FileStream(szDestFile,
FileMode.Create,
FileAccess.Write);
StreamWriter szOutput = new StreamWriter(fsOutput);
for (int nIndex = 0; nIndex < szContents.Count; nIndex++)
{
// Ausgabe einer Zeile in die Ausgabedatei
szOutput.WriteLine(szContents[nIndex]);
}
szOutput.Close();
fsOutput.Close();
// Nachricht über den Programmerfolg
Console.WriteLine("\nDie sortierten Zeilen wurden gesichert\n");
}
}
}
440
Die Programmiersprache C++
8. Die grafischen Bedienoberfächen X und OSF/Motif
8.1 XWindow bzw. X
XWindow oder einfach X193 ist die grafische Benutzeroberfäche für UNIXSysteme194. X ist ein Multi Window-System, das Anwendung und Display auf
verschiedenen Rechnern erlaubt195.
8.1.1 Die Komponenten von X
Das Client-Server-Konzept
Der X-Server
Der X-Server ist ein Programm, das auf dem Rechner laufen muß, auf dessen
Bildschirm (Root-Window) zugegriffen werden soll. Der Serverprozeß heißt X und hat
Kontakt zur Hardware (Bildschirm), aber auch zu Tastatur und Maus. X setzt alle
graphischen Ausgabebefehle der einzelnen Anwendungen in entsprechende
Hardwarebefehle um und verteilt die Eingaben des Benutzers an die
entsprechenden Anwendungen.
Der X-Server stellt den Anwendungen komplexe Datenstrukturen (Ressourcen) zur
Verfügung (z.B. Windows, Pixmaps, GCs und Fonts). Seine Dienstleistungen stellt er
über ein Anwendungsprotokoll, dem X-Protokoll, zur Verfügung. Dabei bedient er
netzweit mehrere Anwendungen (X-Clients) gleichzeitig.
X-Clients
Anwendungen werden im X Window System X-Clients genannt. Sie sind
hardwaremäßig implementiert und senden an den Server nur allgemein gehaltene
Befehle, z.B. „Erzeuge ein Fenster“, „Male einen Kreis“.
Die Kommunikation zwischen X-Server und X-Clients werden gesammelt und in
gebündelten Paketen abgeschickt. Das gleiche gilt für Nachrichten, die vom X-Server
an die Clients geschickt werden.
Ein spezieller X-Client ist der Window-Manager. Er verwaltet die einzelnen
Anwendungen auf dem Bildschirm. Der Window-Manager übernimmt die
Fensterverwaltung und die Gestaltung der Benutzeroberfläche. So erlaubt er das
Verschieben von beliebigen Fenstern (mit der Maus) oder das Vergrößern und
Verkleinern und sorgt auch dafür, daß jedes Fenster einen Rahmen und Titelbalken
bekommt. Über eine Parameter-Datei weiß bspw. der Window-Manager, wie die
193
entwickelt wurde es am MIT, inzwischen ist das Produkt in der Version X11 R6 verfügbar
Die Basis-Software ist public-domain, nur Anwenderprogramme oder konfortablere Oberflächen werden
kommerziell vertrieben, z.B. das Sun Produkt Open Windows
195 vgl. „Einführung in die XWindows-Programmierung“, Uni Regensburg / Physik (zusammengesetllt von F.
Wünsch)
194
441
Die Programmiersprache C++
Bedienoberfläche (Farben, Schriften, ...) aussehen soll. Nach diesen Vorgaben
initialisiert er den Bildschirm.
Wichtige Window-Manager sind: twm, olwm, mwm
Der Anwender sucht sich den Window-Manager aus, der seinen Bedürfnissen 196 am
meisten entspricht.
X-Clients arbeiten ereignisorientiert. Der Server teilt bspw. der Client-Anwendung
die Bewegung des Maus-Zeigers im zur Anwendung zugehörigen Fenster mit. Die
Mausbewegung ist ein Event. Es gibt zahlreiche Ereignisse, die der Server senden
kann. Der Client definiert, welche Typen ihn interesssieren. Nur die bekommt er
dann geliefert. Events werden beim Client automatisch in einer Warteschlange
abgelegt. Ereignisse (Events) können durch Eingaben der Benutzer, durch
Änderungen auf dem Bildschirm oder durch Seiteneffekte von Funktionen ausgelöst
werden. Durch fensterspezifische Ereignis-Masken wird vom X-Client jeweils
festgelegt, welches Fenster über welche Ereignisse informiert werden soll.
Fenster
Ein beliebig großer rechteckiger Bereich auf dem Bildschirm, der vom X-Server (als
Server-Ressource) verwaltet wird, ist ein Fenster (Datentyp: Window). Alle Fenster
eines Bildschirms bilden eine baumartige Hierarchie. Fenster können nur im Bereich
ihres Eltern-Fensters sichtbar sein. Geschwister-Fenster bilden eine Stacking-Order
und können sich gegenseitig überdecken.
Jedes Fenster besitzt verschiedene Attrubute, die die Eigenschaften des Fensters
festlegen. Neben Größe und Position sind dies bspw. auch Rand bzwHintergrundfarben, sowie die Ereignis-Maske, die festlegt, welche Ereignisse für das
Fenster relevant sind.
Fenster können Properties besitzen. Dieser Begriff bezeichnet beliebige Daten,
die an einem Fenster hängen. Jede Property hat einen Namen (, über den sich die
beteiligten Applikationen vorher einigen müssen), einen Typ (, über den sie erst zur
Laufzeit entscheiden brauchen) und einen Wert. Windows-Manager benutzen
Properties zum Austausch von Daten mit anderen Programmen nach den
Vorschriften des Interprocess Communication Manual (ICCM). Dort steht u.a., daß
das Property WM_NAME eines Toplevel-Fensters vom Window-Manager als
Fenstertitel verwendet werden soll. Properties können mit dem X-Utility xprop
ausgegeben werden.
Pixmap und Bitmap
Eine Pixmap ist ein Array mit Werten (Datenstruktur im Off-Screen Bereich des XServer), das üblicherweise als rechteckige Fläche von Farbwerten interpretiert wird.
Cursor
Das ist sichtbare Repräsentation der Mausposition auf dem Bildschirm.
196
Typischerweise startet der Anwender den Window-Manager automatisch beim Login. Danach wird für die
aktuelle „Session“ ein „Look-and-Feel“ festgelegt.
442
Die Programmiersprache C++
Graphics-Context
Jede Ausgabe von Graphik geschieht mit Hilfe eines Graphics-Context (GC). Es
handelt sich dabei um eine vom X-Server angelegte Datenstruktur (Ressource), die
zahlreiche Informationen für Zeichenoperationen enthält, z.B. Vordergrundfarbe,
Linienattribute, Flächenattribute oder Zeichensatz für Textausgaben.
8.1.2 Architektur von X-Programmen
X-Anwendungen verwenden Funktionen aus drei Bibliotheken:
- Xlib (Benutzeroberfläche des X-Window-System)
Sie stellt dem Programmierer verschiedene Lowlevel-Funktionen auf der Ebene des
X-Protokolls zur Verfügung.
- X Toolkit Intrinsics (Aufsatz auf die Xlib)
X Toolkit (Xt) Intrinsics dienen als Werkzeug zum Erzeugen und
Verwalten von grafischen Grundobjekten der Bedienoberfläche, den sog. Widgets.
Widgets197 sind bspw. Buttons, Menüs, Scrollbars, Dialoganwendungen oder
Fenster zum Eingeben und Editieren von Text. Die Xt Intrinsics sind ein
Zusatz zur Xlib und vereinachen das Ereugen von typischen grafischen
Grundobjekten
- Widget-Sets
Das ist eine Sammlung von Widget-Beschreibungen (sog. Widget-Klassen, die von
Widget-Designern entworfen wurden). Für die Xt Intrinsics stehen verschiedene Widget-Sets198 zur Auswahl. Das M.I.T hatte selbst einen eigenen Widget-Set
mit dem Namen „Athena Widget Set“ entwickelt. Weitere, bereits kommerzielle
Widget Sets sind Motif (von der OSF199) und Open Look (von AT&T). Die Widgets
sind in diesen Widget-Sets so implementiert, daß sie das „Look und Feel“ der
entsprechenden Bedienoberfläche unterstützen. Sie harmonisieren z.B jeweils mit
einem speziell für die Oberfläche entwickelten Window-Manager.
Zusammenfassung der Begriffe „window“ und „gadget“ (Fenster und „Dingsbums“).
z.B. Athena Widget Set vom MIT, Motif von der OSF, Open Look von AT&T
199 OSF = Open Software Foundation, inzwischen hat sich OSF/Motif als Standard implementiert
197
198
443
Die Programmiersprache C++
8.1.3 Ein X-Programm
Das Programm zeichnet eine Hilbert-Kurve in ein von X bereitgestelltes Fenster und
zeigt Befehle, die praktisch in jedem X-Programm vorkommen.
1. Include-Dateien
Die komplexen X-Datenstrukturen werden zu Beginn des Programms über
„include“-Anweisungen bekannt gemacht
/* 2*/ #include <X11/Xlib.h> // Definition von X-Datenstrukturen
/* 3*/ #include <X11/Xutil.h> //
"
"
"
2. Kontaktaufnahme zum Server der lokalen Maschine
XOpenDisplay ist der allererste X-Befehl. Er bezieht sich im Programm auf eine
Benutzervariable mydisplay (Zeiger auf eine X-Struktur vom Typ Display)
/* 7*/ Display
*mydisplay; // Basisstruktur fuer Serververbindung
...............
/*80*/
mydisplay = XOpenDisplay("");
/*81*/
myscreen = DefaultScreen(mydisplay); // Bildschirm-Nr
XOpenDisplay stellt die Verbindung mit dem X-Server her. ““ (Leerstring) zeigt an,
daß der Server auf dem gleichen (lokalen) Rechner benutzt werden soll, auf dem
auch der Client läuft. Kommt es zu keinem Kontakt, wird NULL zurückgegeben.
3. Definition des Fenster
Mit XCreateSimpleWindow wird ein (einfaches) Fenster erzeugt, aber noch nicht
angezeigt.
/* 7*/
/* 8*/
/*71*/
/*75*/
/*84*/
/*85*/
/*86*/
/*87*/
Display
*mydisplay; // Basisstruktur fuer Serververbindung
Window
mywindow;
// Window-ID
int
myscreen;
unsigned long
black, white;
mywindow
= XCreateSimpleWindow(mydisplay,
DefaultRootWindow(mydisplay),
100, 300,640, 480, 17,
black, white); // Definition Fenster
DefaultRootWindow bestimmt die Position des neuen Fensters in der WindowHierarchie. Die darauf folgenden Argumente sind Integer-Größen und beschreiben
die Ursprungs-Koordinaten des Fensters. (0,0) liegt am Bildschirm links oben.Die
folgenden zwei Integer-Größen definieren die Größe des Fensters in Pixel. Es folgt
die Randbreite in Pixel. Die beiden letzten Argumente enthalten Kennziffern für die
Farbe des Rands und des Fenster-Hintergrunds.
4. Hinweise an den Window-Manager
444
Die Programmiersprache C++
Informationen über jedes Fenster kann der Window-Manager über eine Struktur im
X-Server mit dem Namen Properties erhalten, die jeder Client abfragen kann. Zwei
solche Properties sind:
/*88*/
/*89*/
XStoreName(mydisplay, mywindow, "Graphik");
XSetIconName(mydisplay, mywindow, "Draw");
„Graphik“ ist der „window-name“, „Draw“ ist der „icon-name“.
5. Anzeigen des Fenster
Das durch XCreateSimpleWindow definierte Fenster wird mit XMapRaised
angezeigt.
/*97*/
XMapRaised(mydisplay, mywindow); // Anzeigen des Fenster
6. Ende der X-Sitzung
// Schliessen des Fenster
/*116*/
XDestroyWindow(mydisplay, mywindow);
// Beenden des Server-Kontakts
/*117*/
XCloseDisplay(mydisplay);
7. Setzen der Eventmaske
Der Client muß (am besten vor dem Anzeigen des Fenster) eine Maske der Events
dem Server übermitteln, auf die er reagieren möchte. Zum Setzen der Event-Maske
dient der Befehl XSelectInput. Die verschiedenen Masken müssen bitweise in
folgender Form verknüpft werden:
/* 7*/ Display
*mydisplay; // Basisstruktur fuer Serververbindung
/* 8*/ Window
mywindow;
// Window-ID
/*94*/
// Setzen der Event-Maske
/*95*/
XSelectInput200( mydisplay, mywindow,
/*96*/
ButtonPressMask | ExposureMask);
Event-Typ
Expose
ButtonPress
ButtonRelease
EnterNotify
KeyPress
Masken-Name
ExposureMask
ButtonPressMask
ButtonReleaseMask
EnterWindowMask
KexPressMask
Anwendung soll zeichnen
Maustaste gedrückt
Maustaste wieder loslassen
Maus hat Fenster erreicht
Taste gedrueckt
Abb. 6.1-1: Ausgewählte Events: Masken und Typen
8. Eventbearbeitungsschleife
200
Dieser befehl sollte vor XMapRaised im Programm stehen, da XMapRaised schon das Expose-Event
sendet und damit anzeigt, daß das Fenster wirklich erzeugt ist.
445
Die Programmiersprache C++
Das ist das Programmstück, das nach Aufforderung durch den Server etwas in das
Fenster zeichnet, auf Tastendruck oder Mausklick reagiert. X-Routinen sollten immer
in einer Schleife mit folgendem Format angeordnet sein:
/* 7*/ Display
*mydisplay;
// Basisstruktur fuer Serververbindung
/*72*/
/*73*/
XEvent
int
myevent;
done;
/*98*/
/*99*/
/*100*/
/*101*/
/*102*/
/*103*/
/*104*/
/*105*/
/*106*/
/*107*/
/*108*/
/*109*/
/*110*/
/*111*/
/*112*/
/*113*/
// Eventbearbeitungsschleife
done = 0;
while (done == 0)
{ // Hole naechsten Eintrag aus dem Puffer
XNextEvent(mydisplay, &myevent);
switch(myevent.type)
{
case Expose:
if (myevent.xexpose.count == 0)
paint();
break;
case ButtonPress:
done = 1;
break;
}
}
Mit XNextEvent wird ein Event aus dem Puffer geholt bzw. gewartet bis einer
ankommt. „myevent.type“ zeigt auf den ersten Eintrag der Event-Struktur. In „switch“
sollten für alle möglichen bzw. erlaubten Events Reaktionen vorgesehen sein. Mit
XEventsQueued wird die Anzahl der Einträge in der Warteschlange ermittelt, z.B.:
Display
*mydisplay; // Basisstruktur fuer Serververbindung
int zahl;
zahl = XEventsQueued(mydisplay,0);
Zur Bearbeitung der Events in einer anderen Reihenfolge, wie sie in der
Warteschlange vorliegen, existieren eine ganze Reihe von Befehlen:
-
Mausknopf-Events
Falls eine beliebige Maustaste gedrückt bzw. losgelassen wird und sich der
Mauszeiger innerhalb des zugehörigen Fensters befindet, dann werden die
Events ButtonPress bzw. BottonRelease erzeugt. Die Eventstruktur liefert
(u.a):
-- myevent.xbutton.button(int)
(gibt an, welche Maustaste es war (links = 1 .. rechts = 3)
-- myevent.xbutton.x bzw. .y(int)
(gibt an, wo sich der Mauszeiger201 zum Zeitpunkt der Aktion befand)
-
Tastatur-Events
-
Expose-Event
So ein Event wird immer gesendet, falls die Anwendung den Fensterinhalt neu
zeichnen soll. Dies ist der Fall, wenn
201
in Pixel zum Window-Ursprung links oben
446
Die Programmiersprache C++
-- das Fenster gerade erzeugt wurde
-- eine Ikonisierung vorlag
-- das Fenster von einem fremden Fenster überdeckt wurde
-- das Fenster verkleinert / vergrößert wurde.
9. Erstellen von Grafik
Der grafische Kontext (graphical context, GC)
Im GC werdem Zeichenattribute festgelegt, z.B.:
-
Farbe für Vorder- und Hintergrund202
XOR-Modus beim Pixel-Setzen ein/aus
Linienbreite, Strichen ein/aus
Der Befehl zum Erzeugen eines GC heißt XCreateGC. Im einfachsten Fall zeigt er
folgenden Ausprägung:
/* 7*/ Display
/* 8*/ Window
/* 9*/ GC
/*91*/
-
-
*mydisplay;
mywindow;
mygc;
// Basisstruktur fuer Serververbindung
// Window-ID
mygc = XCreateGC(mydisplay, mywindow, 0, 0);
In einer Zeichnung können beliebig viele GCs benutzt werden
Einzelattribute eines GC können beliebig gesetzt / geändert werden. Für alle
existieren Default-Werte, d.h. nur die Atrribute müssen explizit angesprochen
werden, die Veränderungen ausdrücken
Jede Zeichenoperation übermittelt nur die Nummer des benutzten GC an den
Server
Ein GC braucht Speicherplatz. Das Freisetzen von Speicherplatz übernimmt
XFreeGC(mydisplay, mygc);
Ändern von Attributen zum Zeichnen
1. Farbe
Zum Zeichnen wird als Default die Farb-Nummer 1, für den Fensterhintergrund die
Farb-Nummer 0 benutzt. Explizit kann man sich die Farbnummern beschaffen über
/*75*/
unsigned long
/*82*/
/*83*/
black
white
black, white;
= BlackPixel(mydisplay, myscreen);
= WhitePixel(mydisplay, myscreen);
Kennt man die Nummern der Farben, kann man sie im GC eintragen mit
XSetForeGround bzw. XSetBackGround, z.B.
/*92*/
/*93*/
202
XSetForeground(mydisplay, mygc, black);
XSetBackground(mydisplay, mygc, white);
Es gibt weitere Attribute, z.B. das Muster für das Auffüllen von Flächen
447
Die Programmiersprache C++
2. Linienattribute
XSetLineAttributes bestimmt, wie Linien aussehen sollen:
unsigned int line_width;
int line_style;
XSetLineAttributes(mydisplay, mygc, line_width, line_style, 1, 0)
„line_width“ sollte für schnelles Zeichen immer 0 sein, dann wird die minimale
Linienbreite benutzt. „Zahlen > 0“ definieren die Breite in Pixel. Für „line_style“ kann
man die vordefinierten Konstanten LineSolid oder LineOnOffDash hernehmen.
Übersicht zu den Grafik-Routinen
XDrawPoint
XDrawPoints
XDrawLine
XDrawSegments
XDrawLines
XDrawArc
XDrawArcs
XFillArc
XFillArcs
XDrawRectangle
XDrawRectangles
XFillPolygon
XClearArea
XClearWindow
setzt einen Punkt
setzt mehrere Punkte
zeichnet eine Linie
zeichnet mehrere getrennte Linien
zeichnet eine verbundene Folge von Linien
zeichnet einen Bogen
zeichnet Bögen / Kreise (Umrisse)
füllt einen Bogen
füllt mehrere Bögen
zeichnet ein Rechteck
zeichnet mehrere Rechtecke
zeichnet ausgefüllte Fläche
löscht rechteckigen Bereich
löscht das Fenster
Abb. 6.1-2: Einfache Grafik-Routinen aus der XLib
-
Zu allen Funktionen203 muß ein GC angegeben werden, dessen Attribute dann
auch zum Zeichnen benutzt werden.
Die aktuelle Window-Größe wird insofern berücksichtigt, daß jede Zeichenaktion
an den Grenzen des Fensters beendet wird. Eine Umskalierung bzw.
Vergrößern/Verkleinern des Fensters findet nicht statt.
Ein vollständiges Demonstrationsprogramm: Hilbert-Kurve
/*
/*
/*
/*
/*
/*
1*/
2*/
3*/
4*/
5*/
6*/
#include
#include
#include
#include
#include
<iostream.h>
<X11/Xlib.h> // Definition von X-Datenstrukturen
<X11/Xutil.h> //
"
"
"
<stdlib.h>
<math.h>
203
Die Versorgung der Funktionen mit Parametern, die Wirkungsweise dieser Funktionen sind umfassend in den
man-Pages dokumentiert.
448
Die Programmiersprache C++
/* 7*/
/* 8*/
/* 9*/
/*10*/
/*11*/
/*12*/
/*13*/
/*14*/
/*15*/
/*16*/
/*17*/
/*18*/
/*19*/
/*20*/
/*21*/
/*22*/
/*23*/
/*24*/
/*25*/
/*26*/
/*27*/
/*28*/
/*29*/
/*30*/
/*31*/
/*32*/
/*33*/
/*34*/
/*35*/
/*36*/
/*37*/
/*38*/
/*39*/
/*40*/
/*41*/
/*42*/
/*43*/
/*44*/
/*45*/
/*46*/
/*47*/
/*48*/
/*49*/
/*50*/
/*51*/
/*52*/
/*53*/
/*54*/
/*55*/
/*56*/
/*57*/
/*58*/
/*59*/
/*60*/
/*61*/
/*62*/
/*63*/
/*64*/
/*65*/
/*66*/
/*67*/
/*68*/
/*69*/
/*70*/
/*71*/
/*72*/
/*73*/
Display
Window
GC
*mydisplay;
mywindow;
mygc;
// Basisstruktur fuer Serververbindung
// Window-ID
#define WIDTH 640.0
#define HEIGHT 480.0
int i, stufe;
// x1, y1 funktioniert nicht,
// weil bereits in LIB. libm.a verwendet. */
float x_1, x_2, y_1, y_2, r, r_1, r_2, h;
void generiere( float r_1, float r_2)
{
stufe--;
if (stufe > 0)
generiere(r_2,r_1);
x_2 += r_1; y_2 += r_2;
XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320),
(int)(240-y_1),
(int)(x_2+320),
(int)(240-y_2) );
x_1 = x_2; y_1 = y_2;
if (stufe > 0)
generiere(r_1,r_2);
x_2 += r_2; y_2 += r_1;
XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320),
(int)(240-y_1),
(int)(x_2+320),
(int)(240-y_2) );
x_1 = x_2; y_1 = y_2;
if (stufe > 0)
generiere(r_1,r_2);
x_2 -= r_1; y_2 -= r_2;
XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320),
(int)(240-y_1),
(int)(x_2+320),
(int)(240-y_2) );
x_1 = x_2; y_1 = y_2;
if (stufe > 0)
generiere(-r_2, -r_1);
stufe++;
}
void paint(void)
{
h = 2;
i = stufe;
while (i > 1)
{
h *= 2;
i--;
}
r = 400 / h;
x_1 = -200; x_2 = -200;
y_1 = -200; y_2 = -200;
generiere(r, 0);
}
main()
{
int
XEvent
int
myscreen;
myevent;
done;
449
Die Programmiersprache C++
/*74*/
/*75*/
/*76*/
/*77*/
/*78*/
/*79*/
/*80*/
/*81*/
/*82*/
/*83*/
/*84*/
/*85*/
/*86*/
/*87*/
/*88*/
/*89*/
/*90*/
/*91*/
/*92*/
/*93*/
/*94*/
/*95*/
/*96*/
/*97*/
/*98*/
/*99*/
/*100*/
/*101*/
/*102*/
/*103*/
/*104*/
/*105*/
/*106*/
/*107*/
/*108*/
/*109*/
/*110*/
/*111*/
/*112*/
/*113*/
/*114*/
/*115*/
/*116*/
/*117*/
/*118*/
/*119*/ }
/*120*/
unsigned long
int
black, white;
i;
cout << "Stufen: "; cin >> stufe;
// X-Sitzung eröffnen
mydisplay = XOpenDisplay("");
myscreen = DefaultScreen(mydisplay); // Bildschirm-Nr
black
= BlackPixel(mydisplay, myscreen);
white
= WhitePixel(mydisplay, myscreen);
mywindow
= XCreateSimpleWindow(mydisplay,
DefaultRootWindow(mydisplay),
100, 300,640, 480, 17,
black, white); // Definition Fenster
XStoreName(mydisplay, mywindow, "Graphik");
XSetIconName(mydisplay, mywindow, "Draw");
// Erzeugen "graphical context"
mygc = XCreateGC(mydisplay, mywindow, 0, 0);
XSetForeground(mydisplay, mygc, black);
XSetBackground(mydisplay, mygc, white);
// Setzen der Event-Maske
XSelectInput( mydisplay, mywindow,
ButtonPressMask | ExposureMask);
XMapRaised(mydisplay, mywindow); // Anzeigen des Fenster
// Eventbearbeitungsschleife
done = 0;
while (done == 0)
{ // Hole naechsten Eintrag aus dem Puffer
XNextEvent(mydisplay, &myevent);
switch(myevent.type)
{
case Expose:
if (myevent.xexpose.count == 0)
paint();
break;
case ButtonPress:
done = 1;
break;
}
}
// Schliessen der Fenster
XFreeGC(mydisplay, mygc);
XDestroyWindow(mydisplay, mywindow);
XCloseDisplay(mydisplay); // Beenden des Server-Kontakts
exit(0);
450
Die Programmiersprache C++
8.2 OSF/Motif
8.2.1 Einführung in OSF/Motif, Xt Intrinsics
Im Mai 1988 schlossen sich sieben verschiedene Hardwarehersteller zur Open
Software Foundation (OSF) zusammen. Ziel dieses Zusammenschlusses war es
eine UNIX Bedienoberfläche zu erschaffen. Sie wurde Mitte 1989 unter dem Namen
OSF/Motif bereitgestellt. Zu OSF/Motif gehören:
-
Das OSF/Motif Widget-Set
Der Motif Window-Manager (mwm)
Die User Interface Language (UIL)
Ein Styl Guide mit Stil-Richtlinien für Motif-Oberflächen
Da Motif auf dem X-Window System aufsetzt, werden die sowohl die Xlib als
auch die Xt Intrinsics in Programmen verwendet. Zur Unterscheidung welche
Bibliothek eine Funktion oder einen Datentyp definiert, einigte man sich auf folgende
Namenskonvention:
-
Alle Xlib-Funktionen und Xlib-Strukturtypen beginnen (bis auf wenige Ausnahmen) mit einem
großen X
Alle Xt Intrinsics-Funktionen und Strukturtypen beginnen (bis auf wenige Ausnahmen) mit Xt
Alle Motif Bezeichnungen beginnen mit Xm
Durch das Zusammenspiel von Motif, Xlib und Xt Intrinsics stehen dem Programmierer mehrere fundamentale Datentypen zur Verfügung:
Xlib Datentyp
Display
Font
GC
Bedeutung
Display- Beschreibung
Zeichensatz
Umgebung für
Zeichenoperationen
Muster beim X-Server
Fenster beim X-Server
universeller Zeiger
Art
Struktur
ID
Zeiger
Xt Datentyp
Boolean
Bedeutung
Art
char bis long
Cardinal
Dimension
Pixel
Position
String
Widget
WidgetClass
XmString
Laufindex
Größenangabe
Farbwert
Koordinate
Zeichenfolge
Fensterbeschreibung beim XClient
Fenstertypbeschreibung
String mit Formatierung
XtPointer
universeller Zeiger
Pixmap
Window
XPointer (seit X11R4)
Wahrheitswert204
ID
ID
char*
unsigned int
int
unsigned long
short / int
char*
Zeiger
Zeiger
CompoundStrin
g
void*
Der hier interessanteste Datentyp ist der Xt Datentyp „Widget“ bzw. „WidgetClass“ .
Die Instanzen des Datentyps Widget beschreiben immer einen Bereich des
204
Für den Datentyp Boolean sind die Konstanten „True“, „TRUE“ (!= 0) „False“, „FALSE“ (== 0) vordefiniert
451
Die Programmiersprache C++
Bildschirms. Ein Widget kann zur Darstellung von Text, zur Darstellung einer
Menüleiste oder zur Einteilung des Bildschirms benutzt werden. Man unterscheidet
verschiedene Arten von Widgets:
-
-
-
Display-Widgets (Primitive Widgets)
stellen ein konkretes Bedienelement dar, das man auf dem Bildschirm sieht. Sie besitzen
Ausgabefunktionalität, zu der im allg. auch eine Eingabefunktionalität gehört. Sie können keine
„Kind“-Widgets besitzen.
Typische Beispiele: Buttons, Menüpunkte, Scrollbars oder Widgets zum Editieren von Text.
Container-Widgets
verwalten das geometrische Layout (Größe und Position) der ihnen untergeordneten („Kind-“)
Widgets.
Shell-Widgets
sind eine spezielle Form von Container-Widgets und übernehmen die Kommunikation mit der
Außenwelt (speziell mit dem Window-Manager). Ihre Fenster sind die Toplevel-Fenster der XAnwendung.
Eine typische X-Anwendung besteht also aus einer baumartigen Hierarchie von
Widgets. das oberste Widget eines solchen Widget-Baums ist ein Shell-Widget.
darunter liegen Container-Widgets, die das Layout einer Anwendung immer weiter
aufteilen. Die Blätter des Widgetbaums sind schließlich Display-Widgets, die die
eigentliche Schnittstelle zu den Funktionen der Anwendung bilden.
8.2.2 Struktur eines Intrinsics-Programmes
Programmrumpf von Intrinsics-Programmen
Im Prinzip sieht der Programmrumpf bei allen Intrinsics-Programmen gleich aus:
-
Einbinden der Headerdateien für Motif ( <Xm/XmAll.h> bindet alle erforderlichen Dateien ein)
Einlesen der Fallback-Ressourcen
Programminitialisierung (meist mit XtAppInitialize())
Erzeugen und ausgeben eines Widgetbaums auf der erhaltenen Applikations-Shell
Eintritt in „Endlosschleife“ zur Ereignissbehandlung (mit XtAppMainLoop())
Das Aussehen der Oberfläche und deren Funktionalität legt allein der Widgetbaum
fest. Die Wurzel dieses Baumes ist immer die Application-Shell. Sie ist bereits ein
Widget, das durch die Funktion XtAppInitialize() Gestalt erhält.
Die Hauptschleife zur Ereignisbehandlung sendet eintreffende X-Events an die
zugehörigen Widgets, wo sie entsprechend der Funktionalität des Widgets
weiterverarbeitet werden, z.B. unter anderem Callbacks auslösen.
Beispiel eines Instrinics-Programmes
// Allgemeine Header-Dateien einbinden
#include<stdio.h>
452
Die Programmiersprache C++
#include<Xm/XmAll.h>
// Beginn des Hauptprogrammes
main(int argc, char* argv[])
{
XtAppContext
kontext;
Widget applShell, text;
// Programminitialisierung und ApplicationsShell erzeugen
applShell = XtAppInitialize (&kontext, "Beispiel1", (XrmOptionDescRec*)
NULL, 0, &argc, argv, (String) NULL, (Arg*) NULL, 0);
// Widgetbaum erzeugen, hier nur ein Textwidget
text = XtCreateManagedWidget ("Beispieltext", xmLabelWidgetClass,
applShell,(Arg*) NULL, 0);
// Widgetbaum realisieren
XtRealizeWidget(applShell);
// Hauptschleife mit Ereignisbehandlung
XtAppMainLoop(kontext);
}
Widgets und Widgetbaum
Widgets können erzeugt, verwaltet (wird im Layout des Elternwidgets berücksichtigt),
realisiert (das zugehörige X-Fenster wird erzeugt) und ausgegeben (auf dem
Bildschirm „sichtbar“) werden. Dazu gibt es entsprechende Grundfunktionen:
Funktionsname
XtCreateWidget
XtManageChild
XtRealizeWidget
XtCreateManagedWidget
XtMapWidget
Auswirkung
liefert als Returnwert ein Widget
berücksichtigen des Kindes im Layout des Eltern Widgets
erzeugt X-Fenster zum Widget
erzeugt und managt ein Widget
macht ein Widget sichtbar
XtDestroyWidget
XtUnrealizeWidget
XtUnmanageChild
XtUnmapWidget
zerstört den Teilbaum
hebt die Realisierung wieder auf
hebt das Management des Widgets wieder auf
macht ein Widget unsichtbar
XtIsManaged
XtIsRealized
liefert zurück ob ein Widget gemanagt wird
liefert zurück ob ein Widget realisiert ist
Im Normalfall (falls keine anderen Ressourcen gesetzt wurden) wird ein Widget
sichtbar sobald es erzeugt, verwaltet und realisiert wurde. Selbstverständlich muß
natürlich das jeweilige Elternwidget auch sichtbar sein, und es darf nicht durch ein
Geschwisterwidget überdeckt werden.
Über Vererbungsmechanismen kann ein Widget verschiedene Eigenschaften (Ressourcen) vom jeweiligen Elternwidget erben.
Ressourcen und Callbacks
Jedes Widget besitzt Parameter, die dessen konkrete Eigenschaften definieren.
Diese Parameter nennt man Ressourcen.
Welche Ressourcen ein Widget besitzt richtet sich nach deren Widgetklasse, aber
alle Ressourcen der darüberliegenden Widgetklassen werden geerbt. So erhalten
453
Die Programmiersprache C++
alle Widgets die Ressourcen der Widgetklasse Core die als Basisklasse aller Widgets
dient. Die wichtigsten Ressourcen der Basisklasse Core sind:
Ressource-Konstante
XmNx
XmNy
XmNwidth
XmNheight
XmNbackground
XmNborderWidth
XmNscreen
XmNmappedWhenManaged
XmNcolormap
...
Datentyp
Position
Position
Dimension
Dimension
Pixel
Dimension
Screen*
Boolean
Colormap
...
Defaultwerte
0
0
Label-Breite
Label-Breite
verschieden
0
XtCopyScreen
True
XtCopyFromParent
...
Möglichkeiten zum Manipulieren von Ressourcen
Es gibt zwei Arten die Ressourcen von Widgets zu manipulieren:
-
Innerhalb des Programmes, diese können von außen nicht mehr geändert werden
Außerhalb des Programmes, diese können auch nach der Übersetzung des Programmes
überschrieben werden. Sie werden in spezielle Ressource-Dateien festgelegt, und zur Startzeit des
Programmes gelesen
Ressourcen-Dateien
Zum Setzen von Ressourcen außerhalb des Programmes dienen vor allem
Ressource-Dateien. Der Aufbauen einer solchen Datei muß einer speziellen Syntax
gehorchen: In jeweils einer Zeile einer Ressource-Datei wird festgelegt, in welchem
Programm, in welchen Widget, welche Ressource welchen Wert bekommt.
Ressource Zeilen haben folgenden Aufbau:
Programmname.Widgetpfad.Widget.RessourceKonstante:
Wert
Unter dem Begriff „Widgetpfad“ ist der Pfad im Widgetbaum gemeint um zu dem
speziellen Widget zu gelangen. Zu beachten ist auch, daß die Ressourcekonstante
ohne die Präfix „XmN“ angegeben wird.
Um zum Beispiel das Textwidget des Beispielprogrammes1 auf „Hello World“ zu
setzen, genügt in der Ressource-Datei folgende Zeile:
Beispiel1.text.labelString:
Hello World
Es besteht die Möglichkeit im „Widgetpfad“ Wildcards zu benutzen, um mehrere
Widgets gleichzeitig anzusprechen. Zum einen das Fragezeichen, das im Pfad
genau ein Widget ersetzt, zum anderen der Stern, der einen beliebigen Pfad
repräsentiert. Hierbei kann selbst der Programmname entfallen. Die RessourceZeile:
*text.labelString:
Hello World
454
Die Programmiersprache C++
setzt zum Beispiel in allen Programmen in denen die Ressource-Datei verwendet
wird, in jedem Widget mit dem Namen „text“ die Ressource „labelString“ auf
den Wert: „Hello World“.
Durch ein Ausrufezeichen in einer Ressource-Zeile kann Kommentar eingeleitet
werden, der bis zum Zeilenende geht. Ein Ausrufezeichen innerhalb eines
Ressource-Wertes ist allerdings Teil dieses Wertes.
Um nun diese Ressource-Dateien im Programm verwenden zu können, wird intern
eine Ressource-Datenbasis durch die Initialisierungsfunktion erzeugt. Nur wenn eine
Ressource für ein Widget benötigt wird, wird ermittelt welche dieser RessourceZeilen für das Widget von Bedeutung sind. Im allgemeinen gibt man solchen
Application-Defaults-Ressource-Dateien die Endung „ad“. Sie muß aber in ein
spezielles Directory für Ressource-Dateien z.B.: „usr/lib/X11/app-defaults“
und unter dem Klassennamen des Programmes z.B.: „Beispiel1.ad“ installiert
werden. Die Initialisierungsfunktion „XtAppInitialize“ füllt nun die Datenbasis mit
den Ressource-Zeilen der Datei auf. Dies geschieht durch die Angabe des
Klassennamens im zweiten Parameter.
Setzen von Ressourcen im Programm
Innerhalb eines Intrinsics-Programmes hat der Applicationsprogrammierer zwei
Möglichkeiten Widget-Ressourcen zu setzen. Zum einen bei der Erzeugung eines
Widgets, zum anderen durch eine spezielle Funktion. In allen Fällen muß als
Parameter ein Array von speziellen Strukturen, eine sogenannte Argument-Liste,
übergeben werden. Zum Auffüllen dieses Arrays empfiehlt sich ein vordefiniertes
Makro „XtSetArg“. Die Anwendung dieses Makros führt zu folgendem typischen
Aufbau beim Setzen von Widget-Ressourcen.
Arg
args[10];
Cardinal
n;
//Argument-Liste
//Laufvariable
n = 0;
XtSetArg(args[n], RessourceKonstante1, Wert1); n++;
XtSetArg(args[n], RessourceKonstante2, Wert2); n++;
Somit wird durch das Makro die Argument-Liste aufgefüllt, wobei die Ressource
„RessourceKonstante“ den Wert „Wert“ zugeordnet bekommt. Um diese Ressourcen
für ein Widget zu setzen gibt es wie schon erwähnt zwei Möglichkeiten:
Falls die Ressourcen beim Erzeugen des Widgets gesetzt werden sollen, werden die
letzten zwei Parameter der Funktion „XtCreateWidget“ benutzt:
widget = XtCreateWidget(....., args, n);
wobei „args“ den Zeiger auf die Argument-Liste repräsentiert, und „n“ den
Laufindex.
Falls die Ressourcen eines existierenden Widgets geändert werden sollen, wird die
Funktion „XtSetValues“ benutzt. Diese Funktion erwartet als Parameter auch die
schon beschriebene Argument-Liste und den Laufindex erwartet. So setzt:
XtSetValues(widget, args, n);
die Ressourcen des Widgets „Widget“, so wie in der Argument-Liste „args“
beschrieben.
455
Die Programmiersprache C++
Abfragen von Ressourcen
Für das Abfragen von Ressourcen gibt es ebenfalls eine spezielle Funktion
„XtGetValues“. Sie liest die Werte von Ressourcen über die schon erläuterte
Argument-Liste aus. Die einzelnen Argumente bestehen allerdings aus dem
Ressource-Namen (RessourceKonstante) und der Adresse der Variablen in der Wert
der Ressource eingetragen werden soll. Man kann also nicht mit der gleichen
Argument-Liste Ressourcen setzten und abfragen.
Zum Abfragen der Größe eines Widgets dienen z.B. folgende Zeilen:
Arg args[10];
Cardinal
n;
Dimension b;
Dimension h;
//Argumentliste
//Laufindex
//auszulesende Breite
//auszulesende Höhe
n = 0;
XtSetArg (args[n], XmNwidth, &b); n++;
XtSetArg (args[n], XmNheight, &h); n++;
XtGetValues (widget, args, n)
Callback-Ressourcen
Um in Intrinsics-Programmen eine Funktionalität implementieren zu können, gibt es
die Möglichkeit, einem Widget als Ressource Adressen von Funktionen zu
übergeben, die dann unter bestimmten Bedingungen aufgerufen werden. Man
spricht hierbei von ereignisorientierter Programmierung. Da dem Widget die Adresse
der Funktion übergeben wird nennt man solche Parameter auch Callbacks.
Callbackressourcen sind einfache Ressource-Konstanten. Sie hängen aber sehr eng
mit dem benutzen Widget zusammen. So hat ein Widget der Klasse
„XmPushButton“ eine RessourceKonstante „XmNactivateCallback“, mit denen
die Callbackfunkionen verbunden werden, die dann ausgeführt werden sollen, falls
der Button gedrückt und wieder losgelassen wurde205.
Eine Callbackfunktion darf kein Ergebnis zurückliefen (void) somit im Sinne von
Paskal eine Prozedur sein. Da sie von internen Funktionen aufgerufen werden, sind
auch die Parameter vordefiniert. Somit ergibt sich folgender Aufbau für eine
Callbackfunktion:
void callbackFunktion (Widget widget, XtPointer clientDaten, XtPointer
aufrufDaten)
Im ersten Parameter wird der Funktion das aufrufende Widget übergeben. Die
beiden folgenden Parameter sind Zeiger beliebigen Typs, werden sie benötigt, ist
eine Typumwandlung erforderlich (sogenanntes Typcasting).
Um nun Callbacks bei einem Widget zu registrieren gibt es eine spezielle Funktion
„XtAddCallback“, um diese Verbindung wieder aufzulösen die Funktion
„XtRemoveCallback“. Neben diesen zwei wichtigsten gibt es noch viele andere,
die aber über Callback-Listen arbeiten, auf die hier nicht näher eingegangen wird.
205
Eine Auflistung der verschiedenen Callbackressourcen der verschiedenen Widgets folgt im Abschnitt
„OSF/Widget Set“.
456
Die Programmiersprache C++
Um in einem Programm einen Widget Exit-Button „exit“ mit einer ExitCallbackfunktion „exitCB“ zu verbinden benötigt man folgende Zeilen:
XtAddCallback(exit, XmNactivateCallback, exitCB, (XtPointer) NULL);
wobei im letzten Parameter die XtPointer Variable für die Übergabe an die
exitCB Funktion steht. Da sie hier nicht benutzt wird, wird durch einen Typcast der
NULL-Zeiger übergeben. In der Funktion „exitCB“ wird z.B. die exit() Funktion
aufgerufen, was zu einer ordentlichen Programmbeendung führt.
8.2.3 Das OSF/Widget Set
Kategorien
Widgets lassen sich gemäß ihres typischen Verwendungszweckes in folgende
Kategorien einteilen:
Display-Widgets haben die Eigenschaft, daß man sie auf dem Bildschirm „sehen“
kann. Veränderbare und konstante Texte, Knöpfe zum „draufdrücken“, Grafiken,
Listen usw. sind einige Vertreter dieser Gruppe. Jedes „Blatt“ des Widgetbaumes
muß folglich ein Display-Widget sein.
Container-Widgets dienen vor allem dem Zweck, das geometrische Layout ihrer
Kinder zu kontrollieren. Sie ordnen ihre Kind-Widgets in Zeilen und Spalten oder
untereinander an oder stellen Ressourcen zur Verfügung, durch die sich ein Kind
Widget abhängig von seinen Geschwistern ausrichten kann.
Shell-Widgets sind zwar ebenfalls Container, sie besitzen jedoch nur ein einziges
gemanagtes Kind-Widget. Eine der wesentlichen Aufgaben ist es, die Verbindung
mit der „Außenwelt“ herzustellen. Dies bedeutet, beispielsweise dem WindowManager Hinweise zu übermitteln, wie „wichtig“ die in der darunterliegenden
Widgethierarchie dargestellte Information ist, oder ob sie beispielsweise nur kurz
auf dem Bildschirm sichtbar sein wird.
Dialog-Widgets werden häufig dazu benutzt, relativ kurze Dialoge mit dem
Benutzer abzuwickeln, und werden deshalb nur bei Bedarf auf den Bildschirm
ausgegeben. Informationen, Fehlermeldungen, Dateinamen usw. können somit
ausgegeben bzw. erfragt werden. Dialog-Widgets sind Container und somit um
Kind-Widgets erweiterbar.
Überblick über alle Widgets
Die folgende Aufstellung zeigt die wichtigsten Widgetklassen unterteilt in Display,
Shell, Container Widgets:
DisplayWidgets:
WidgetKlassen-Name
Beschreibung
Motif-Version
457
Zeile im
Die Programmiersprache C++
XmArrowButton
XmScrollBar
XmLabel
XmList
XmSeperator
XmText
XmTextField
XmCsText
XmPushButton
XmToggleButton
XmDrawButton
XmCascadeButton
Bedienknopf mit Pfeil (Datensatznavigation)
Verschiebeleiste
Text oder Muster
Liste von auswählbaren Texten
Trennlinie
Text
einzeiliger Texteditor
ein oder mehrzeiliger Texteditor mit
Formatierungsmöglichkeit
Taster
Schalter
Button mit Bild
Menüpunkt mit Untermenue
1.0
1.0
1.0
1.0
1.0
1.0
1.1
2.0
Beispiel
76
-
1.0
1.0
1.0
1.0
241
237
wird nicht vom Windowmanager verwaltet
für Menüs
legt fest wie der Windowmanager mit einem
Widget umgehen soll
legt Merkmale im Zusammenspiel mit
Windowmanager fest
temporäre Dialoge mit dem Benutzer
temporäre Dialoge mit dem Benutzer
zur Verwendung mehrerer Widgetbäume
in ihr läuft der Hauptteil der Applikation ab
1.0
1.0
1.0
320
-
1.0
-
1.0
1.0
1.0
1.0
74
191
1.0
1.0
1.0
1.0
1.0
1.0
1.0
2.0
2.0
390
371
74
383
-
1.0
-
ShellWidgets:
OverrideShell
XmMenuShell
WmShell
VendorShell
TransientShell
XmDialogShell
TopLevelShell
ApplicationShell
ContainerWidgets:
XmDrawingArea
XmRowColumn
XmForm
XmMessageBox
XmFileSelectionBox
XmSelectionBox
XmScrolledWindow
XmSpinBox
XmNotebook
XmMainWindow
X-Window
Anordnung der Kinder in Zeilen und Spalten
Anordung der Kinder durch „berühren“
Nachrichten
Fenster mit Dateiauswahlmöglichkeit
Fenster mit Auswahlmögichkeit
Window mit Bildlaufleisten
zyklisches Durchlaufen einer Wertemenge
Simuliert eine Buch das seitenweise
durchgeblättert werden kann
typisches Hauptfenster, mit festen Plätzen
für Kinder
Die wichtigsten WidgetKlassen und ihre Ressourcen
In diesem Abschnitt werden die wichtigsten Widgetarten genauer erläutert,
insbesondere ihre speziellen Ressourcen aufgezeigt.
XmLabel-Widget
458
Die Programmiersprache C++
Eines der wichtigsten Display-Widgets ist die Klasse „XmLabel“. Sie werden benutzt
um Text darzustellen, ohne das hier eine Eingabe des Benutzers erwartet wird. Sie
dient aber auch als Oberklasse für alle „Button - Widgets“.
Der Text im Label selbst ist ein Compound-String, d.h. er kann formatiert werden.
Neben den Ressourcen der Oberklassen besitzt das Label-Widget folgende spezielle
Ressourcen206:
XmNLabelString: Ist vom Datentyp XmString und enthält den darzustellenden Text
XmNLabelTyp: Für diese Ressource gibt es zwei mögliche Werte, XmSTRING
(Default) zur Darstellung von Text und XmPIXMAP zur Darstellung eines Bilds.
XmNLabelPixmap:Ist vom Typ Pixmap, dort muß das darzustellende Bild angegeben werden
XmNalingnment:Diese Ressource steuert die Ausrichtung des Textes, möglich sind
XmNALLIGNMENT_CENTER zum Zentrieren des Texte XmNALLIGNMENT_BEGINNING
für linksbündig und XmNALLIGNMENT_END für rechtsbündingen Text.
XmNrecomputeSize:Diese Ressource ist vom Typ Boolean und steuert mit True
und False ob sich der Text bei Größenänderung des Widgets automatisch mit
anpaßt.
XmPushButton-Widget
Durch das dreidimensionale Erscheinungsbild dieses Knopfes entsteht beim
Anwender die Vorstellung, das durch das Drücken des Knopfes ein Kommando
ausgelöst werde. Da diese Klasse eine Unterklasse der Label-Widgets ist, besitzt sie
alle zuvor beschriebenen Ressourcen, dazu erhält er spezielle Callback-Ressourcen
die genau steuern, wann welche Callback-Funktion auszulösen ist:
XmNactivateCallback:Diese Callbacks werden aufgerufen, wenn der Benutzer den
Mausknopf überhalb des Button-Widgets drückt, und dort auch wieder losläßt.
XmNarmCallback: werden aufgerufen wenn der Mausbutton innerhalb des Widgets
gedrückt wird.
XmNdisarmCallback:wird aufgerufen wenn der Mausbutton innerhalb des Widgets
gedrückt , aber irgendwo anders wieder losgelassen wurde
XmToggleButton-Widget
Widgets dieser Klasse sind Ein/Aus-Schalter. Sie können zwei verschiedene
Zustände anzeigen. Die Callback-Ressourcen sind dieselben wie beim PushButton,
nur
die
Ressource
XmNactivateCallback
hat
den
Namen
XmNvalueChangedCallback erhalten. Beim XmNToggleButton-Widget sind vor
allem die Ressourcen zur Darstellung der Zustände interessant:
XmNSet: ist vom Typ Boolean und gibt an ob gerade der Ein- oder der Auszustand
gewählt ist.
206 Neben diesen Ressourcen gibt es natürlich noch viele andere mehr, sie sind in der Fachliteratur
dokumentiert.
459
Die Programmiersprache C++
Es gibt auch hier noch viele weitere Ressourcen, unter anderem gibt es seit Motif 2.0
die Möglichkeit nicht nur zwei, sondern drei Zustände über einen Schalter zu
steuern. Ebenfalls ist es möglich Kombinationen von ToggleButtons zu erstellen, von
denen genau einer den Zustand „Ein“ haben kann.
XmCascadeButton-Widget
Ein XmCascadeButton-Widget hat normalerweise eine Menüleiste oder ein
anderes Menü als ElternWidget. Eine Aktivierung dieses Knopfes führt im
allgemeinen dazu, das eine Untermenü erscheint.
XmNsubMenuId: Diese Ressource erwartet die Angabe eines Widgets zur
Präsentation des Untermenüs, das bei Aktivierung aufgeklappt werden soll
XmText-Widget
Ein Widget dieser Klasse ist dazu gedacht Texte einzulesen, die der Anwender
eingibt. Dem Anwender kann man hierzu einen kleinen Texteditor zur Verfügung
stellen. Hier die wichtigsten Ressourcen eines XmText-Widgets:
XmNvalue: enthält den vom Anwender eingegebenen Text, ist aber kein CompoundString, sondern nur ein einfacher String
XmNeditMode: mit dieser Ressource kann man über die Werte
XmSINGLE_LINE_EDIT und XmMULTI_LINE_EDIT steuern wieviele Zeilen der
Text haben darf.
XmNeditable:gibt über True und False an, ob der Text editierbar ist.
Um weiterführende Editorfunktionen zu erhalten, gibt es seit Motif 2.0 die XmCSTextWidgets. Diese speichern den eingegebenen Text als Compound-String, deshalb
sind dort Textformatierungen möglich.
Nebenbei sei noch erwähnt das sich der kleine Texteditor zwischen Einfüge- und
Überschreibmodus mit der Funktion „toggle-overstrike()“ umschalten läßt.
XmList-Widget
In einem XmList-Widget können, untereinander in Zeilen angeordnet, Texte
aufgelistet werden, wobei mit der Maus eine oder mehrere Zeilen ausgewählt werden
können. Eine typische Verwendung ist dieses Widget in einer FileSelectionBox zur
Auswahl der Datei. Diesem Widget stehen sehr viele Ressourcen zur Verfügung,
hier die wichtigsten:
XmNitems: ist ein Zeiger auf eine Liste von Compound-Strings, die die Einträge der
Liste repräsentiert.
XmNitemCount: ist die Anzahl der Einträge in XmNitems
460
Die Programmiersprache C++
XmNvisibleItemCount: gibt an wieviele Einträge in der Liste sichtbar sein sollen, es
beeinflußt somit die Höhe des Widgets.
XmNselectionPolicy: über diese Ressource kann man steuern, ob Einzelauswahl
(XmSINGLE_SELECT) oder Mehrauswahl (XmMULTIPLE_SELECT) möglich ist
XmNselectedItems:ist ein XmStringTable der die ausgewählten Einträge enthält.
Im Zusammenhang mit diesem Widget gibt es noch einige Funktionen:
XmListAddItem: fügt einen Item in die Liste ein.
XmListDeleteItem: löscht einen Eintrag aus der Liste.
XmListSelectItem: selektiert einen Eintrag anhand seines Textes
XmListDeselectItem: hebt Selektierung des Eintrages wieder auf.
XmListItemExists: Liefert True falls der Text ein Eintrag in der Liste ist
XmCreateScrolledList: erzeugt Instanzen eines XmList-Widgets als Kind eines
ScrolledWindow-Widgets207
XmSeperator-Widget
Mit XmSeperator-Widgets können als optische Trennung horizontale oder vertikale
Linien dargestellt werden. Sie dienen oft dazu Einteilungen des Bildschirms, z.B.
funktionale Gruppierungen in Menüs, vorzunehmen. Diese Widgetklasse besitzt
selbst nur drei spezielle Ressourcen:
XmNmargin: vom Typ Dimension gibt die Ausdehnung an
XmNorientation: steuerbar
XmNseperatorTyp: gibt den Linientyp an
XmSINGLE_LINE eine einfache Linie
XmDOUBLE_LINE eine doppelte Linie
XmSINGLE_DASHED_LINE eine einfach gestrichelte Linie
XmSHADOW_ETCHED_LINE_IN eine Linie „ins Fenster“
XmSHADOW_ETCHED_LINE_OUT eine Linie „aus dem Fenster“
Für XmSeperator-Widgets gibt es keine Callbacks oder Reaktionen auf irgendwelche Benutzereingaben.
XmSelectionBox-Widget
In einem XmSelectionBox-Widget kann dem Benutzter eine Liste von Einträgen
angeboten werden, aus denen er einen Eintrag selektieren kann. Diese ausgewählte
Eintrag wird in ein editierbares Text-Widget übernommen. Per Default stehen
außerdem noch vier Buttons für OK, Apply, Cancel, Help zur Verfügung.
Verschiedene Ressourcen steuern Layout und Funktion:
XmNapplyCallback: Gibt den Callback für den Applybutton an.
XmNlapplyLabelString: Gibt die Beschriftung für den Applybutton an. Analog
lassen sich die Labels für den OK, Cancel, Help Button angeben.
207 Die Syntax und Funktionsweise kann den man-Pages entnommen werden.
461
Die Programmiersprache C++
XmNdialogType: damit läßt sich steuern welche Kinder vorhanden sein sollen und
welche nicht. bei XmDIALOG_SELECTION werden alle Kinder erzeugt und verwaltet.
Bei XmDIALOG_WORK_AREA werden alle erzeugt, aber nur der Applybutton verwaltet.
Bei XmDIALOG_PROMT fehlen das List-Widget und dessen Überschrift. Wird häufig
für einzeilige Benutzereingaben verwendet.
Um andere Dialog-Typen zu erhalten, gibt es die Möglichkeit, über die Funktion
„XmSelectionBoxGetChild“ einzelne Kinder zu entfernen, bzw. umzuformen. Die
Kind-Komponente läßt sich über folgende Konstanten angeben:
XmDIALOG_APPLY_BUTTON
XmDIALOG_LIST
XmDIALOG_LIST_LABEL
XmDIALOG_TEXT
XmDIALOG_SELECTION_LABEL
für den Applybutton und analog für die anderen Buttons.
für das XmList-Widget
für die Überschrift des XmList-Widgets
für das Text-Widget
für die Überschrift des XmText-Widgets.
XmFileSelectionBox-Widget
Diese Widgetklasse erlaubt dem Benutzer das Navigieren durch Directories. Er kann
auch einen Pfadnamen angeben, und einen Filter aktivieren. Dieses Widget enthält
wiederum die Buttons für OK, Apply (Filtern), Cancel und Help. Die speziellen
Ressourcen sind:
XmNfileTypeMask: damit läßt sich steuern, ob in der Liste nur Dateien
(XmFILE_REGULAR) oder nur Directories (XmFILE_DIRECTORY) oder beides
(XmFILE_ANY_TYPE) zu sehen sein soll
XmNnoMatchString:Text der angezeigt werden soll, wenn es keine Datei oder
Directory gibt zu dem das angegebene Suchmuster paßt
Selbstverständlich steht auch hier eine Funktion zur Verfügung die einzelne KindWidgets ansprechbar macht. Die Funktion „XmFileSelectionBoxGetChild“
erfüllt diese Aufgabe. Die Kind-Widgets können über folgende Konstanten
angesprochen werden:
XmDIALOG_APPLY_BUTTON
XmDIALOG_FILTER_LABEL
XmDIALOG_FILTER_TEXT
XmDIALOG_DIR_LIST
XmDIALOG_LIST
XmDIALOG_TEXT
...
für den Applybutton und analog für die anderen Buttons.
für die Überschrift über dem Dateifilter
für den Dateifilter
ist das List-Widget für die Navigation
ist das List-Widget für das Ergebnis der Filterung
ist das Text-Widget, das die Benutzerauswahl aus den beiden
Listen enthält
XmMessageBox-Widget
Instanzen der Widgetklasse XmMessageBox können dazu benutzt werden um kurze
Dialoge mit dem Benutzer abzuwickeln. Eine MessageBox enthält die Buttons OK,
Cancel und Hilfe einen String mit der eigentlichen Nachricht, und ein Symbol für
Warnung, Information usw. Die neuen Ressourcen dieser Widgetklasse sind:
XmNokCallback: Der Callback für den Ok-Button. Analog für die anderen Buttons.
XmNokLabelSring: Die Beschriftung des Ok-Buttons. Analog für die anderen
Buttons.
XmNdialogType: Hiermit läßt sich ein Symbol auswählen das angezeigt werden soll:
XmDIALOG_MESSAGE
XmDIALOG_WARNING
für kein Symbol
für ein Ausrufezeichen
462
Die Programmiersprache C++
XmDIALOG_QUESTION
für einen Kopf mit Fragezeichen
XmDIALOG_INFORMATION für ein großes I
XmDIALOG_ERROR
für ein Stop Symbol
XmDIALOG_WORKING
für eine Sanduhr
XmNmessageString: Der Text der erscheinen soll
Durch die Angabe von XmDIALOG_TEMPLATE können Dialoganfragen selbst designt
werden.
Um auf die Ressourcen der Kind-Widgets zuzugreifen zu können müß auch hier eine
Funktion benutzt werden, die das Kind-Widget zurückliefert. Die Kind-Widgets
können über folgende Konstanten angesprochen werden:
XmDIALOG_OK_BUTTON für den Ok-Button und analog für die anderen Buttons.
XmDIALOG_MESSAGE_LABEL für das Text-Widget, das den Messagestring enthält.
XmRowColumn-Widget
Dieses Widget ist ein Container Widget. Es besitzt viele Ressourcen, die verschiedene Layout-Strategien für die Kind-Widgets steuern. Hier die wichtigsten LayoutRessourcen:
XmNorientation: durch die Angabe von XmHORIZONTAL oder XmVERTICAL läßt
sich steuern ob die Kind-Widgets untereinander oder nebeneinander angeordnet
werden sollen
XmNpacking: mit der Angabe von XmPACK_COLUMN erreicht man daß die Kinder in
gleiche große Boxen gesetzt werden. Typisch für Menüs. Durch die Angabe von
XmPACK_NONE behält jedes Kind seine Position wie in der Ressource XmNx und
XmNy angegeben
XmNnumColumns: gibt die Anzahl der Zeilen bei horizontaler Ausrichtung an.
XmNspacing: gibt die Dimension des Zwischenraumes, zwischen den Kind-Widgets
an
XmForm-Widget
Diese Container-Widgetklasse ermöglicht eine weitere Layoutstrategie. Diese
Strategie ermöglicht es den Kind-Widgets einen Nachbarn anzugeben, nach dem
sich das Widget zu orientieren hat. Dazu erhält das Kind-Widget folgende neue
Ressourcen:
XmNseiteAttachment: Bei Angabe von XmATTACH_FORM richtet es sich nach dem
betreffenden Rand des XmForm-Widgets aus. XmATTACH_WIDGET stellt eine
Verbindung zur entgegengesetzten Seite des in der folgenden Ressource
angegebenen Widgets her. XmATTACH_NONE läßt es ohne Ausrichtung.
XmNseiteWidget: Das Widget richtet sich nach dem hier angegebenen
Geschwister-Widget, falls XmATTACH_WIDGET oben angegeben wurde.
XmNrezisable: Diese Ressource vom Typ Boolean definiert, ob ein von ihm selbst
kommender Wunsch nach Größenänderung ausgeführt wird, oder nicht.
463
Die Programmiersprache C++
Hierbei steht „Seite“ für top, bottom, left oder right, denn alle vier Seiten eines
Widgets können von anderen Widget-Seiten abhängig gemacht werden.
Die Widgetklasse XmForm selbst, besitzt folgende neue Ressourcen:
XmNverticalSpacing: hier wird der Default-Abstand nach links und rechts zwischen
dem Widget und seinem Attachment angegeben
XmNhorizontalSpacing: hier wird der Default-Abstand nach oben und unten
zwischen dem Widget und seinem Attachment angegeben.
XmDrawingArea - Widget
Mit einem Widget dieser Klasse soll dem Applikationsprogrammierer eine Widgetart
bereitgestellt werden, die eigentlich nicht mehr Funktionalität besitzt als ein Window,
aber in die Widgethierarchie eingebunden ist. Hauptsächlich werden
XmDrawingAreas benutzt um Grafiken darzustellen. Die neuen Ressourcen dieser
Widgetklasse sind:
XmNexposeCallback: wird aufgerufen falls der Fensterinhalt neu ausgegeben
werden muß.
XmNresizeCallback: wird aufgerufen falls sich die Größe des Fensters ändert.
XmNinputCallback: wird aufgerufen falls Tastatureingaben oder Mauklicks
erfolgten.
XmNresizePolicy: diese Ressource definiert inwiefern das Widget von sich aus
seine Größe zu ändern versucht. XmRESIZE_NONE bedeutet, daß das Widget seine
Größe nicht von sich aus ändert. XmRESIZE_ANY veranlaßt das Widget seine Größe
so zu ändern, daß alle Kind-Widgets genau Platz haben. XmRESIZE_GROW führt zu
einem automatischen Wunsch nach Vergrößerung
Da für Zeichenoperationen und Textausgaben mit den Funktionen der Xlib als
Parameter eine Window-ID, ein Grafikkontext und ein Display nötig sind, stellt Motif
einige Funktionen zur Verfügung, durch die man diese Parameter erhalten kann:
XtDisplay(Widget) liefert das Display
XtWindow(Widget) liefert die Window-ID
XtGetGC(Widget, werteMaske, Werte) liefert den Grafikkontext
Für die Erzeugung des Grafikkontexts ist die Abfrage der Vorder- und
Hintergrundfarbe nötig. Diese Werte sind dann in die Wertestruktur einzutragen.
Diese Struktur ist vom Typ „XGCValues“ und enthält „foreground“ und „background“
vom Datentyp „Pixel“.
Menüleisten und Pulldown-Menüs
Das OSF/Motif Widget-Set besitzt verschiedene Widgetklassen, die die Verwendung
von Menüs in Programmen unterstützen. Folgende Widgetklassen besitzen spezielle
Funktionalitäten für Menüs.
464
Die Programmiersprache C++
XmRowColumn: Sie dienen grundsätzlich als Container-Widgets für Menüs.
XmCascadeButton: Haben die Aufgabe Menüs automatisch auszugeben.
XmMenuShell: Sind spezielle Shell-Widgets für Menüs, die als Popups ausgegeben
werden
Eine Standard-Anwendung von Menüs ist die Verwendung einer Menüleiste, in der
die einzelnen Menüpunkte Pulldown-Menüs ausgeben. Bei der Implementierung
eines solchen Menüpunktes muß nur angegeben werden, welcher Menüpunkt
welches Menü aktiviert. Alles weitere geschieht automatisch durch die Verwendung
des „XmCascadeButtons“. Dieses Widget besitzt dazu die Ressource
XmNsubMenuId in die das dazugehörige Container-Widget des dazugehörigen
Pulldown-Menüs eingetragen wird. Dieses Sub-Menu wird in Abhängigkeit von der
Menüart, in dem sich der Button befindet, aktiviert:
Befindet sich der CascadeButton in einer Menüleiste, wird das dazugehörige
Pulldown-Menü durch Drücken des Buttons aktiviert.
Befindet sich der CascadeButton in einem Pulldown-Menü, so wird das Sub-Menü
durch drüberstreichen mit der Maus ausgegeben. Auf diese Weise kann eine
Kaskade (geschachtelte Folge) von Menüs entstehen. Daher hat das Widget auch
seinen Namen.
Das Erzeugen einer Menüleiste mit Pulldown-Menüs geht denkbar einfach:
//Menüleiste erzeugen:
menueLeiste = XmCreateMenuBar(elternWidget, "Menüleiste",(Arg*) NULL,0);
//Menüleiste managen:
XtManageChild(menueLeiste)
//Pulldown-Menü erzeugen:
dateiMenue = XmCreatePulldownMenu (menueLeiste, "Datei", (Arg*) NULL,0);
//Menüpunkt innerhalb des Untermenüs erzeugen hier "Öffnen":
button = XtCreateManagedWidget ("Punkt", xmPushButtonWidgetClass,
dateiMenue, (Arg*)
NULL,0);
//Pulldown-Untermenü dazu erzeugen:
speicherMenue = XmCreatePulldownMenu(dateiMenue, "Speichern", (Arg*)
NULL,0);
//Untermenuepunkt "Speichern erzeugen":
button1 = XtCreateManagedWidget("Speichern", xmPushButtonWidgetClass,
speicherMenue, (Arg*) NULL,0);
//Pulldow-Untermenü "speichern als ..." erzeugen:
button2 = XtCreateManagedWidget("Speichern als ...",
xmPushButtonWidgetClass, speicherMenue, (Arg*) NULL,0);
//Menüpunkt "speichern" erzeugen der dieses Untermenü aufruft:
n = 0;
XtSetArg(args[n], XmNsubMenuId, speicherMenue); n++;
XtCreateManagedWidget("Speichern", xmCascadeButtonWidgetClass, dateiMenue,
args, n);
//Menüpunkt "Datei" erzeugen, der dieses Pulldown-Menue aufruft:
n = 0;
XtSetArg(args[n], XmNsubMenuId, dateiMenue); n++;
XtCreateManagedWidget ("Datei", xmCascadeButtonWidgetClass, menueLeistem,
args, n)
465
Die Programmiersprache C++
466
Herunterladen