1 OBJEKTORIENTIERTE MODELLIERUNG...................................... 7 2

Werbung
OOM/OOP
1
1
OBJEKTORIENTIERTE MODELLIERUNG...................................... 7
1.1
Grundbegriffe der Objektorientierung ...................................................... 8
1.1.1
Objekte und Klassen............................................................................................... 8
1.1.2
Vererbung............................................................................................................. 10
1.1.3
Nachrichten .......................................................................................................... 10
1.1.4
Polymorphismus................................................................................................... 11
1.2
Prinzipien der Objektorientierung ........................................................... 11
1.2.1
Abstraktion ........................................................................................................... 11
1.2.2
Einkapselung ........................................................................................................ 12
1.2.3
Hierarchie ............................................................................................................. 12
1.2.4
Modularisierung ................................................................................................... 13
1.3
Objektorientierte Modellierung................................................................ 13
1.3.1
Objektorientierte Analyse (OOA) ........................................................................ 15
1.3.2
Objektorientiertes Design (OOD) ........................................................................ 17
1.3.3
Objektorientierte Programmierung (OOP)........................................................... 19
1.4
Objektorientierte Modellierungstechniken ............................................. 19
1.4.1
OOAD nach Coad & Yourdon ............................................................................. 20
1.4.2
OMT nach Rumbaugh et al .................................................................................. 24
1.4.3
OOAD nach Booch .............................................................................................. 26
1.4.4
Vergleich der Verfahren....................................................................................... 28
2
2.1
3
OBJEKTORIENTIERTE PROGRAMMIERSPRACHEN ................. 33
Objektorientierte Modellierung im Bauwesen ........................................ 33
DIE PROGRAMMIERSPRACHE C / C++....................................... 35
3.1
Allgemeines............................................................................................... 35
3.2
Erstes C++ -Programm............................................................................. 36
3.3
Programmaufbau ...................................................................................... 38
3.4
Interne Darstellung von Werten............................................................... 40
3.4.1
Zeichen ................................................................................................................. 40
3.4.2
Ganzzahlen ........................................................................................................... 41
3.4.3
Gleitkomma-Zahlen ............................................................................................. 41
3.4.4
Zeichenketten ....................................................................................................... 42
3.5
Sprachsymbole ......................................................................................... 42
3.5.1
Namen (Bezeichner)............................................................................................. 43
3.5.2
Reservierte Wörter ............................................................................................... 43
3.5.3
Numerische Konstanten ....................................................................................... 43
3.5.4
Ganzzahlige Konstanten....................................................................................... 43
3.5.5
Literale (Zeichenketten) ....................................................................................... 44
3.5.6
Trenner ................................................................................................................. 45
2
OOM/OOP
3.6
Datenobjekte und L-Werte ....................................................................... 45
3.6.1
Variable ................................................................................................................ 45
3.6.2
Deklaration von Variablen ................................................................................... 46
3.6.3
Deklaration von Variablen ................................................................................... 47
3.6.4
Datentypen ........................................................................................................... 47
3.6.5
Datentyp bool ....................................................................................................... 47
3.6.6
Datentyp int .......................................................................................................... 47
3.6.7
Datentyp char ....................................................................................................... 48
3.6.8
Datentyp float ....................................................................................................... 48
3.7
Programmierstil ........................................................................................ 48
3.8
Formatierte Ein- und Ausgaben .............................................................. 49
3.9
Operatoren ................................................................................................ 54
3.9.1
Liste der Operatoren............................................................................................. 55
3.9.2
Arithmetische Operatoren .................................................................................... 56
3.9.3
Logische Operatoren ............................................................................................ 57
3.9.4
Zuweisungsoperatoren ......................................................................................... 57
3.9.5
Adress-Operator & ............................................................................................... 57
3.9.6
Dereferenzoperator *............................................................................................ 58
3.9.7
Der scope-resolution Operator „::“ ...................................................................... 58
3.10
Klassen I .................................................................................................... 59
3.10.1 Die Klasse ............................................................................................................ 59
3.10.2 Konstruktoren und Destruktoren.......................................................................... 61
3.11
Typkonvertierungen ................................................................................. 64
3.11.1 Implizite Typkonvertierungen.............................................................................. 64
3.11.2 Explizite Typkonvertierung.................................................................................. 64
3.12
Anweisungen ............................................................................................ 65
3.12.1 Ausdrucksanweisung............................................................................................ 65
3.12.2 Leere Anweisung.................................................................................................. 66
3.12.3 Funktionsaufrufe .................................................................................................. 66
3.12.4 Wertzuweisung..................................................................................................... 66
3.12.5 Blockanweisung ................................................................................................... 67
3.12.6 Bedingte Anweisungen ........................................................................................ 68
3.12.7 Wiederholungsanweisungen................................................................................. 71
3.12.8 Sprunganweisungen ............................................................................................. 75
3.13
Funktionen ................................................................................................ 79
3.13.1 Funktionsdefinition .............................................................................................. 81
3.13.2 Funktionsdeklaration............................................................................................ 82
3.13.3 Funktionsaufrufe .................................................................................................. 83
3.13.4 Default-Parameterwerte ....................................................................................... 85
3.13.5 Overloading von Funktionen................................................................................ 86
3.13.6 Inline Deklarationen............................................................................................. 86
3.13.7 const-Deklaration ................................................................................................. 86
3.14
Klassen II ................................................................................................... 87
3.14.1 Inline Methoden ................................................................................................... 87
OOM/OOP
3.14.2
3
Überlagerung von Methoden................................................................................ 87
3.15
Vererbung.................................................................................................. 88
3.15.1 Syntax (einfache Vererbung) ............................................................................... 88
3.15.2 Schutzkonzept ...................................................................................................... 89
3.15.3 Offene (public) und geschlossene (private) Vererbung ....................................... 89
3.15.4 Beispiel Basisklasse ............................................................................................. 89
3.15.5 Beispiel spezialisiertere Klasse ............................................................................ 90
3.16
Einfache und mehrfache Vererbung ....................................................... 90
3.16.1 Syntax................................................................................................................... 90
3.16.2 Abstrakte Basisklassen......................................................................................... 90
3.16.3 Virtuelle Destruktoren.......................................................................................... 91
3.16.4 Virtuelle Basisklassen .......................................................................................... 91
3.17
Polymorphismus (Späte Bindung) .......................................................... 92
3.17.1 Frühe Bindung...................................................................................................... 92
3.17.2 Späte Bindung ...................................................................................................... 92
3.18
Overloading (Überlagerung von Operatoren)......................................... 93
3.19
Friends (Befreundete Funktionen und Klassen) .................................... 96
3.19.1 Befreundete Funktionen ....................................................................................... 96
3.19.2 Befreundete Klassen............................................................................................. 96
3.20
Templates .................................................................................................. 97
3.20.1 Syntax................................................................................................................... 98
3.20.2 Beispiel................................................................................................................. 98
3.21
Exception Handling .................................................................................. 99
3.21.1 Syntax................................................................................................................. 100
3.21.2 Beispiel............................................................................................................... 100
3.22
Run-Time Type Information (RTTI)........................................................ 100
3.22.1 Syntax................................................................................................................. 101
3.22.2 Beispiel............................................................................................................... 101
3.23
Standard Template Library (STL) .......................................................... 102
3.24
Name Spaces .......................................................................................... 102
3.25
Namenskonvention für Bezeichner....................................................... 103
3.25.1 Globale Funktionen, Funktionstemplates........................................................... 104
3.25.2 Membervariablen ............................................................................................... 104
3.25.3 Layout von header-Dateien ................................................................................ 105
3.26
Pointer ..................................................................................................... 105
3.26.1 Dynamische Speicherverwaltung....................................................................... 106
3.26.2 Referenzen.......................................................................................................... 106
3.27
Felder....................................................................................................... 109
3.28
Mehrdimensionale Felder....................................................................... 110
4
OOM/OOP
3.29
Speicherklassen von Variablen ............................................................. 111
3.29.1 Automatische Variablen..................................................................................... 112
3.29.2 Statische Variablen............................................................................................. 112
3.29.3 Schlüsselwörter der Speicherklassen ................................................................. 113
3.29.4 Verwendung bei Variablen................................................................................. 113
3.29.5 Verwendung bei Funktionen .............................................................................. 114
3.30
Bibliotheken ............................................................................................ 114
3.31
Präprozessor........................................................................................... 115
3.32
Makros ..................................................................................................... 116
3.33
#include Anweisung ............................................................................... 117
3.34
Textdateien.............................................................................................. 118
3.35
Strukturierte Datentypen........................................................................ 120
3.35.1 Aufzählungstyp enum ........................................................................................ 120
3.36
Beispiel: enum ........................................................................................ 120
3.36.1 Vereinbarung von Strukturen ............................................................................. 121
3.36.2 Zugriff auf Strukturbereiche............................................................................... 122
4
DIE C++-STANDARDBIBLIOTHEK ............................................. 125
4.1
Allgemeines............................................................................................. 125
4.2
Die Standard-Template-Library (STL) ................................................... 127
4.3
Container-Klassen .................................................................................. 128
4.3.1
Überblick über Container-Klassen ..................................................................... 128
4.3.2
Sequentielle Container ....................................................................................... 129
4.3.3
Assoziative Container ........................................................................................ 130
4.4
Container-Adapter .................................................................................. 131
4.5
Gemeinsame Operationen ..................................................................... 134
4.6
Iteratoren ................................................................................................. 135
4.6.1
Allgemeines........................................................................................................ 135
4.6.2
Beispiele ............................................................................................................. 136
4.6.3
Kategorisierung von Iteratoren........................................................................... 138
4.7
Algorithmen............................................................................................. 139
4.7.1
Allgemeines........................................................................................................ 139
4.7.2
Beispiel:.............................................................................................................. 139
4.7.3
Bereiche in Algorithmen .................................................................................... 140
4.7.4
Beispiel:.............................................................................................................. 140
4.7.5
Funktionen als Parameter von Algorithmen....................................................... 141
4.7.6
Alle Algorithmen im Überblick ......................................................................... 142
4.8
Fehlerbehandlung in der STL ................................................................ 145
OOM/OOP
5
5
DIE MICROSOFT FOUNDATION CLASS LIBRARY ................... 146
5.1
Grundlagen.............................................................................................. 146
5.2
Schlüsselkonzepte ................................................................................. 146
5.3
Arbeiten mit der Programmierumgebung von Visual C++ .................. 147
5.4
Programmvorbereitung .......................................................................... 148
5.5
Programmgerüst..................................................................................... 149
5.6
Ableiten eigener Klassen von der MFC ................................................ 154
5.6.1
Grundlagen ......................................................................................................... 154
5.6.2
Rahmenfenster und unterteilte Fenster (Splitterbox) ......................................... 156
5.6.3
Mehrere Dokumenttypen, Ansichten und Rahmenfenster ................................. 156
6
ANHANG...................................................................................... 158
6.1
6.1.1
6.1.2
Beispiel „Stuetzenprogramm“............................................................... 159
Klasse cStuetzenProg (StuetzenProg.h) ............................................................. 160
Klasse cStuetzenProg (StuetzenProg.cpp) ......................................................... 160
7 ......................................................................................................... 163
7.1.1
7.1.2
7.1.3
7.1.4
8
Klasse cStuetze (Stuetze.h)................................................................................. 163
Klasse cStuetze (Stuetze.cpp)............................................................................. 165
Globale Variablen (Global.h)............................................................................. 168
Hauptprogramm (BspMain.cpp) ........................................................................ 168
WINDOWSPROGRAMMIERUNG (API) ....................................... 171
8.1
Einleitung ................................................................................................ 171
8.2
Elemente einer Windows-Anwendung.................................................. 171
8.3
Interne Abläufe........................................................................................ 175
8.4
Windows-Objekte und Handles ............................................................. 176
8.5
Grundlogik eines Windows-Programms............................................... 176
8.6
Dateien..................................................................................................... 177
8.7
Erstes Windows-Programm ................................................................... 178
8.8
Erläuterungen zur C-Sourcecode-Datei ................................................ 180
9
LITERATUR ................................................................................. 188
OOM/OOP
7
1 Objektorientierte Modellierung
Der objektorientierte Ansatz verspricht für die Softwareentwicklung in den neunziger
Jahren ähnliche Bedeutung zu erlangen wie die strukturierte Programmierung in den
vergangenen Jahrzehnten. Im Gegensatz zur daten- bzw. funktionsorientierten
Strukturierung eines Problems wird in der Objektorientierung ein Modell durch die
Abstraktion des Problembereiches erzeugt. Hierbei sind die wichtigsten Bestandteile
eines objektorientierten Programms Objekte und Klassen, wobei die Objekte der
Software meist direkt den realen Objekten des Anwendungsgebietes entsprechen.
Zum konventionellen Vorgehen besteht also insofern ein Unterschied, als im
objektorientierten Ansatz Daten und darauf operierende Funktionen als Einheit
(Objekt) aufgefasst werden (Abb. 1.1). Anstelle von Funktionsaufrufen (die auf
globalen Daten operieren) tauschen Objekte Nachrichten aus und setzen Aktionen in
Gang. Objekte bzw. Klassen stehen in Beziehungen zueinander und sind in der
Regel in eine Vererbungshierarchie eingebunden.
Konventionell
Beziehungen
Aufrufe
Zugriffe
Funktionen
tauschen
Nachrichten
aus und setzen
damit
Aktionen in Gang
Daten
erben Daten,
Beziehungen,
Funktionen
Objekte
Objektorientiert
Abb. 1.1: Unterschiede zwischen konventionellem und objektorientiertem Ansatz
Systeme die objektorientiert modelliert sind, sind wiederverwendbar, erweiterbar und
leicht wartbar. Gerade komplexe Systeme lassen sich damit übersichtlicher gestalten
und auch dokumentieren.
8
1.1
OOM/OOP
Grundbegriffe der Objektorientierung
Als Grundbegriffe der Objektorientierung können Objekte und Klassen, Vererbung,
Nachrichten und Polymorphismus bezeichnet werden. Diese Elemente kommen in
allen objektorientierten Systemen vor, meist auch mit diesen Bezeichnungen. Im
Nachfolgenden sollen diese grundlegenden Begriffe der Objektorientierung zum
besseren Verständnis der vorliegenden Arbeit erläutert werden.
1.1.1 Objekte und Klassen
Objekte sind informationstechnische Elemente, die die Elemente der realen Welt
direkt in ein Softwaremodell abbilden. Objekte besitzen Daten (Attribute) und
Funktionen (Methoden), wobei auf die Daten nur mittels von außen sichtbaren
Methoden zugegriffen werden kann. Diese Datenkapselung (information hiding) ist
ein Grundprinzip der objektorientierten Softwareentwicklung. Grundsätzlich kann ein
Objekt irgendein individuelles und identifizierbares Exemplar von Dingen, Personen
oder Begriffen der realen Welt repräsentieren.
Ein Objekt kann zum Beispiel:
•
ein Individuum, wie beispielsweise einen Einwohner, einen Mitarbeiter, einen
Professor, usw.
•
ein reales Objekt, wie beispielsweise ein Auto, ein Gebäude, eine Wand, usw.
•
ein abstraktes Konzept, wie beispielsweise ein Fachgebiet, eine Vorlesung, usw.
•
ein Ereignis, wie beispielsweise die Immatrikulation eines Studenten, eine
Rechnungsverbuchung, usw.
•
eine Beziehung, wie beispielsweise eine Ehe, usw.
•
ein Softwarekonstrukt, wie beispielsweise eine verkettete Liste, einen Stapel,
usw.
•
ein Ergebnis, wie beispielsweise eine Bildschirmausgabe, eine Grafik, ein
Formular, usw.
darstellen.
Eine Klasse ist die Beschreibung einer Menge nahezu gleicher Objekte. Eine Klasse
beschreibt die Daten und Methoden, die eine Menge von Objekten charakterisieren.
Die Möglichkeit von einer Menge ähnlicher Objekte Gemeinsamkeiten zu
abstrahieren und in einer Klasse allgemein zu beschreiben ist ein markantes
Merkmal objektorientierter Systeme.
Objekte sind Instanzen von Klassen (konkrete Klassen). Es gibt auch Klassen die
nicht instanziierbar sind, diese nennt man abstrakte Klassen.
Eine Klasse beschreibt ein Objekt, eine Instanz einer Klasse ist ein Objekt.
OOM/OOP
9
Realität
Software-Modell
5
11 er Trennwand
24 er Tragende Wand
Wand
5
36 er Außenwand
Objekte
Klasse
Abb. 1.2: Klassen beschreiben Objekte der Realität
Ein weiteres Konzept sind die sogenannten generischen oder auch
parametrisierten Klassen, die eine ganze Familie von Klassen beschreiben. Über
Parameter muss eine Klasse erst generiert werden, bevor man von dieser Objekte
erzeugen kann. Diese generischen Klassen werden oft zur Implementierung von
Listen, Stacks oder Queus verwendet, die wiederum beliebige Objekte verwalten.
Man spricht hierbei auch von Containerklassen.
Objekte werden durch ihre Attribute (Daten) und Methoden (Memberfunktionen)
beschrieben. Die Attribute bestimmen hierbei den Zustand, in dem das Objekt sich
befindet und die Methoden steuern das Verhalten des Objektes. Im Beispiel könnte
die Klasse Wand die Attribute Dicke, Länge und Höhe besitzen. Als Methoden könnten
Zugriffsfunktionen wie SetzeDicke, GibDicke und andere, wie zum Beispiel
BerechneVolumen definiert sein. Methoden sind Funktionen, die ein Objekt ausführen
kann und welche festlegen, wie ein Objekt auf Nachrichten reagiert. Das Reagieren
auf Nachrichten hängt vom jeweiligen Zustand des Objektes ab, also vom Inhalt
seiner Attribute.
10
OOM/OOP
1.1.2 Vererbung
Die Vererbung ist eines der wichtigsten Konzepte der Objektorientierung. Mit Hilfe
der Vererbung können die Eigenschaften einer Klasse an eine hierarchisch
untergeordnete Klasse weitergegeben werden. Diese abgeleitete Klasse erbt dann
die Datenstruktur (Attribute) und das Verhalten (Methoden) von der sogenannten
Oberklasse. Oberklassen nennt man auch Superklassen. Die Vererbungshierarchie
kann mehrstufig sein, das heißt eine Klasse kann von mehreren Oberklassen
abgeleitet sein. Einfache Vererbung liegt dann vor, wenn eine Klasse von genau
einer Superklasse abgeleitet wird (je Hierarchiestufe, vgl. Abb. 1.3 links). Von
Mehrfachvererbung spricht man, wenn eine Klasse von mehreren Superklassen
innerhalb einer Hierarchiestufe abgeleitet wird (vgl. Abb. 1.3 rechts).
Grafisches
Objekt
Bitmap
Interaktives
Objekt
Kreis
Abb. 1.3: Einfach- und Mehrfachvererbung
In Abb. 1.3 wird die Klasse Bitmap von der Klasse Grafisches Objekt abgeleitet und
erbt somit alle Eigenschaften der Superklasse GrafischesObjekt. Eine Bitmap ist also
auch ein grafisches Objekt. Die Klasse Kreis ist ein grafisches Objekt und soll
gleichzeitig die Eigenschaften eines interaktiven Objektes besitzen. Der Kreis muss
also die Eigenschaften der Klasse GrafischesObjekt und der Klasse InteraktivesObjekt
erhalten. Dies wird hier durch Mehrfachvererbung erreicht. Die Vererbungsbeziehung
zwischen Klassen wird auch als „is a“-Beziehung (aus der Sicht der Unterklasse)
bezeichnet.
Die Möglichkeit der Vererbung ist einer der wesentlichen Vorteile der
objektorientierten
Programmentwicklung,
vor
allem
bezüglich
der
Wiederverwendbarkeit bzw. der Erweiterbarkeit von Software.
1.1.3 Nachrichten
Während in den herkömmlichen Programmen Funktionen aufgerufen werden, spricht
man in der Objektorientierung von Nachrichten, die die Methoden der Objekte
aktivieren. Objekte werden durch solche Nachrichten angestoßen und reagieren
abhängig von ihrem internen Zustand auf diese. Wenn zum Beispiel ein Anwender
ein Objekt Wand dazu veranlasst sein Volumen zu ermitteln, so empfängt die Wand
diese Nachricht z.B. vom Menüsystem der Anwendung und bearbeitet diese
OOM/OOP
11
Nachricht, indem sie ihr Volumen berechnet und danach eventuell einen Dialog
öffnet in dem das Ergebnis angezeigt wird. Wichtig ist hierbei, dass der
Nachrichtensender nicht wissen muss, wie seine Nachricht ausgeführt wird. Das
weiß nur das empfangende Objekt. Eine Unterklasse kann auf Nachrichten auch mit
geerbten Methoden reagieren oder Methoden der Oberklasse überschreiben. Dies
wird dann zur Laufzeit des Programms entschieden.
1.1.4 Polymorphismus
Wie im vorhergehenden Kapitel beschrieben wurde, reagieren Objekte auf
Nachrichten. Ein weiteres Beispiel wäre eine Nachricht drucken, die die
Eigenschaften (Attribute) eines Objektes ausdruckt. Sendet man nun diese Nachricht
drucken an verschiedene Objekte, so können die Reaktionen unterschiedlich sein.
Diese Eigenschaft objektorientierter Programme wird mit Polymorphismus
bezeichnet. Als Beispiel könnte man die Nachricht drucken an eine Wand, Stütze oder
Deckenplatte schicken, welche dann die unterschiedlichsten Informationen
ausdrucken. In strukturierten Programmiersprachen müssen die Funktionen
unterschiedlich benannt werden.
Der eigentliche Vorteil des Polymorphismus liegt beim Einsatz von Klassen, die in
eine Vererbungshierarchie eingebunden sind. Dort werden zentrale Methoden wie
drucken, speichern, laden, usw. so weit oben als möglich in der Hierarchie definiert. Zur
Laufzeit des Programms wird dann erst an Hand des Objektes entschieden, welche
Implementierung von drucken verwendet wird.
Dieser Mechanismus wird mittels des „dynamischen Bindens“ von den Compilern der
verwendeten Programmiersprache realisiert. Wird eine Methode nicht im aktuellen
Objekt gefunden, so wird sie in der nächst höheren Hierarchiestufe gesucht und
dann ausgeführt.
1.2
Prinzipien der Objektorientierung
Der objektorientierte Ansatz gibt dem Softwareentwickler eine der menschlichen
Denkweise angepasste Software-Modellierungsmethode an die Hand. Sie soll vor
allem die Bewältigung der Komplexität von Anwendungssystemen mit Hilfe der
Konzepte wie Abstraktion, Hierarchisierung und Modularisierung verbessern.
Im folgenden werden die Prinzipien der Objektorientierung (vgl.[3]), nämlich
Abstraktion, Einkapselung, Hierarchisierung und Modularisierung, näher
erläutert.
Neben diesen Hauptelementen eines objektorientierten Programms gibt es noch
Elemente wie Typisierung, Nebenläufigkeit und Persistenz.
1.2.1 Abstraktion
Die Abstraktion dient der Zerlegung von Problemen und hilft dem Entwickler, deren
Komplexität besser bewältigen zu können. Mit Hilfe der Abstraktion können
Gemeinsamkeiten von Objekten erkannt und in einer Klasse zusammengefasst
werden. Die Objektorientierung fordert von dem Softwareentwickler, seinen
Problemraum zu abstrahieren und die Abstraktionen als Klassen und Objekte zu
definieren.
12
OOM/OOP
1.2.2 Einkapselung
Das Prinzip der Einkapselung dient der besseren Wiederverwendbarkeit von
objektorientierten Modellen. Hierdurch werden die Daten eines Objektes so
gekapselt, dass kein direkter Zugriff von Außen möglich ist. Das Modifizieren von
Eigenschaften (den Daten) eines Objektes ist nur über die Verwendung seiner
Methoden möglich. Diese Methoden bilden also eine Schnittstelle zu den Daten
eines Objektes. Dadurch ist es möglich die interne Datenstruktur eines Objektes zu
verändern, ohne dass die Verwendung (Aufruf der Methoden, Versenden von
Nachrichten an diese Objekte) dieser Objekte bzw. Klassen geändert werden muss.
Einkapselung wird auch als information hiding bezeichnet. Je nach
Programmiersprache kann die Einkapselung mittels Zugriffsrechten (in C++ außer
private und public auch protected möglich) gesteuert werden.
Im Beispiel ist das Attribut Radius der Klasse Kreis private definiert und kann mit der
Schnittstelle SetzeRadius(...) verändert werden.
class Kreis : public GrafischesObjekt
{
private:
double Radius;
Implementierung
Punkt Mittelpunkt;
int Farbe;
public:
Kreis();
void SetzeMPunkt(double mx, double my);Schnittstelle
void Setze Farbe(int farbe)
};
Abb. 1.4: Beispiel einer Klassendefinition in C++
1.2.3 Hierarchie
Mit Hilfe der Abstraktion können komplexe Probleme zerlegt oder zusammengefasst
werden, wobei sich dann verschiedene Abstraktionsebenen herausbilden können.
Diese Ebenen bilden dann eine Hierarchie, in der die Klassen angeordnet sind. Es
gibt zwei Typen von Hierarchien: die „kind of“-(oder „is a“-) Hierarchie und die „part
of“- Hierarchie (oder „has“-Beziehung). Die „kind of“-Hierarchie entsteht durch die
Generalisierung einer Abstraktion und die „is a“-Hierarchie durch Spezialisierung.
Diese beiden bezeichnen also, je nach Standpunkt (Sicht von der Unterklasse auf die
Oberklasse = Generalisierung, Sicht von der Oberklasse auf die Unterklasse =
Spezialisierung), eine Vererbungshierarchie
Bei der „part of“-Hierarchie handelt es sich um eine Aggregationsbeziehung
(Ganzes/Teil-Hierarchie) zwischen Klassen. Unter Aggregation versteht man, wenn
eine Klasse mittels einer „part of-“ Beziehung in andere Klassen aufgeteilt wird oder
wenn eine konzeptuelle Beziehung zwischen einer und mehreren anderen Klassen
besteht, die nicht unbedingt an ein physikalisches Enthaltensein gebunden ist.
OOM/OOP
13
1.2.4 Modularisierung
Entsprechend schon in der strukturierten Programmierung angewandt, ist das
Konzept der Modularisierung auch in der Objektorientierung ein wichtiges Prinzip.
Das Prinzip der Modularisierung besteht in der Gliederung eines Programms in
kleinere Module, die getrennt übersetzt werden können und mit anderen Modulen in
einer losen Kopplung stehen.
Ändert man Teile eines Moduls, so muss nur dieses neu übersetzt werden. Ändert
man allerdings die Schnittstelle zu diesem Modul, so müssen alle Module, die
Objekte dieses Moduls verwenden, auch neu übersetzt werden.
In der Praxis heißt das, dass jede Klasse einzeln übersetzt werden kann.
Die Unterteilung in Module ist also eine Trennung des Modells auf der physikalischen
Ebene (z.B. Aufteilung in mehrere Programmdateien bzw. Verzeichnisse).
1.3
Objektorientierte Modellierung
Für die Erstellung von objektorientierter Software werden spezielle
Entwurfsmethoden benötigt, mit deren Hilfe man zu einem objektorientierten Modell
kommt, das dann in einer objektorientierten Programmiersprache implementiert wird.
Herkömmliche Entwurfsmethoden sind für den Entwurf objektorientierter Software
unzureichend, weil sich der Aufbau strukturierter Systeme von dem objektorientierter
Programme grundlegend unterscheidet. Im Gegensatz zu den strukturierten
Entwurfsmethoden, sind die Übergänge zwischen den einzelnen Phasen des
objektorientierten Entwurfsprozesses fließend. Darin liegt auch der Vorteil der
objektorientierten Softwaremodellierung.
Analyse
Design
Programmierung
Flugzeug
Flugzeug
Flugzeug
Stack
Stack
Bauteil
Bauteil
Bauteil
Liste
Liste
Brief
Reale Welt
Analysemodell
Brief
Designmodell
Abb. 1.5: Der objektorientierte Entwurfsprozess
Brief
Programmcode
14
OOM/OOP
Die Abbildung der Realität des Problemraumes in eine rechnerinterne Darstellung ist
über alle Phasen des objektorientierten Entwurfsprozesses (vgl. Abb. 1.5) in einem
einzigen Modell, dem objektorientierten Modell, ohne Strukturbruch möglich.
Die drei Hauptphasen des objektorientierten Entwurfs sind die objektorientierte
Analyse (OOA), das objektorientierte Design (OOD) und die objektorientierte
Programmierung (OOP), welche in den nachfolgenden Kapiteln an einem einfachen
Beispiel erläutert werden sollen.
Die einzelnen Phasen der objektorientierten Modellierung (vgl. Abb. 1.6) werden im
ersten Schritt sequentiell durchlaufen, während das weitere Vorgehen iterativ ist. Das
Ergebnis der von der Programmiersprache weitestgehend unabhängigen
Modellierung ist das Objektmodell, welches bei größeren Aufgabenstellungen durch
die parallele Implementierung eines Prototypen überprüft werden kann (Prototyping).
Abb. 1.6: Phasen der objektorientierten Modellierung
Spezielle Notationen (unterschiedlich je nach gewählter Methode) unterstützen den
Softwareingenieur beim Modellieren und vor allem beim Dokumentieren des
gefundenen Modells.
Als Aufgabe soll die Erstellung eines „Grafischen Editors“ dienen, mit dem
Zeichnungen, die grafische Objekte enthalten und eine Bitmap als Hintergrund
haben, erstellt werden können. Um das Beispiel nicht unnötig groß zu machen, soll
hier nur der Kern des Problems, nämlich die grafischen Objekte und deren
Verwaltung, betrachtet werden und nicht noch die Problematik des Graphical User
Interface (GUI) mit hinzugenommen werden. Mit dem grafischen Editor sollen
grafische Objekte wie Linien und Kreise erzeugt und manipuliert werden können. Die
Anzahl der Linien und Kreise soll dabei unbeschränkt sein. Als Hintergrund soll eine
Bitmap dargestellt werden, die unveränderlich ist.
OOM/OOP
15
1.3.1 Objektorientierte Analyse (OOA)
In der objektorientierten Analyse wird der Problemraum untersucht, wobei vor allem
die Frage nach dem „Was?“ im Vordergrund steht. Hauptsächlich werden mittels
verschiedener Vorgehensweisen (Heuristiken, grammatikalische Untersuchung des
Pflichtenheftes, CRC-Methode usw.) Klassen und Objekte des Problemraumes
identifiziert und deren Verhalten und Beziehungen festgelegt. Diese Klassen können
dann zu Gruppen zusammengefasst werden. In einem weiteren Schritt werden dann
die Beziehungen zwischen den gefundenen Klassen, entsprechend den
Anforderungen, definiert. Danach werden die Attribute und Methoden der einzelnen
Klassen und Objekte festgelegt. Innerhalb der Analyse unterscheidet man noch
Teilmodelle wie das statische und dynamische Modell. Die Klassenhierarchie mit
ihren Beziehungsstrukturen ist ein typisches statisches Modell, während die
Instanzen der Klassen, also die Objekte, durch das Versenden und Reagieren auf
Nachrichten, ein dynamisches Verhalten des Modells erzeugen.
Nach einem ersten Durchgang kann das Modell an verschiedenen Stellen durch
Generalisieren oder Spezialisieren verfeinert werden. Diese Verfeinerung geschieht
oft mehrmals und hängt natürlich auch von der Definition der Attribute und Methoden
der einzelnen Klassen ab. In Abb. 1.7 sind die wichtigsten Schritte der
objektorientierten Analyse noch einmal in ihrer Bearbeitungsreihenfolge
zusammengestellt.
Objektorientierte Analyse
‹ Analyse des Problembereichs
‹ Finden von Klassen und Objekten
‹ Zusammenfassen zu Gruppen
‹ Identifizieren von Beziehungen
‹ Vererbung
‹ Aggregation
‹ Assoziation
‹ Definieren von Attributen
‹ Definieren von Methoden
Abb. 1.7: Objektorientierte Analyse
OOA des Beispiels „Grafischer Editor“:
•
Analyse des Problembereiches
•
Finden von Klassen und Objekten
16
OOM/OOP
GrafischerEditor, Zeichnung, Punkt, Linie, Kreis, Bitmap
•
Zusammenfassen von Gruppen
Gruppe „Hauptelemente“: GrafischerEditor, Zeichnung
Gruppe „Grafische Objekte“: Punkt, Linie, Kreis, Bitmap
•
Identifizierung von Beziehungen
•
Vererbung (Generalisierung, Spezialisierung)
Gemeinsame Eigenschaften der Klassen Punkt, Linie, Kreis, Bitmap können
in der Oberklasse GrafischesObjekt zusammengefasst werden. Da die
Objekte (außer der Bitmap) manipuliert werden sollen, kann man die
interaktiven Eigenschaften in der Oberklasse InteraktivesObjekt definieren.
•
Aggregation („has-“ Beziehung)
Da eine Linie sich aus zwei Punkten (Anfangs- und Endpunkt)
zusammensetzt kann hier eine Aggregationsbeziehung definiert werden.
Auch der Kreis besteht aus Mittelpunkt und Radius.
Eine Zeichnung aggregiert sich aus den Zeichnungselementen, also den
grafischen Objekten.
Die Klasse GrafischerEditor wiederum aggregiert sich aus Zeichnungen.
•
Assoziation (semantische Beziehung)
Als Assoziation kann zum Beispiel eine Gruppierungsklasse Gruppe definiert
sein oder „Linie liegt an Kreis im Punkt“.
•
Definieren von Attributen
Hier werden die Attribute der Klassen definiert. Beispielhaft sind in Abb. 1.8 hier nur
wenige Attribute je Klasse aufgeführt.
Zum Beispiel das Attribut Sichtbar der Klasse GrafischesObjekt.
•
Definieren von Methoden
Methoden für die Klasse GrafischerEditor können zum Beispiel Neuzeichnen,
Drucken, Selektieren, Verschieben und Löschen sein. Oder zum Beispiel die Methode
Zeichnen der Klasse GrafischesObjekt als virtuelle Methode.
OOM/OOP
17
GrafischerEditor
Methoden:
- Neuzeichnen
- Drucken
- Selektieren
- Verschieben
- Löschen
Zeichnung
Eigenschaften:
- Liste mit
Grafischen
Objekten
- Zeichnung
...
Methoden:
- GibName
...
Eigenschaften:
- Aktiviert
- Name
...
GrafischesObjekt
Methoden:
- Zeichnen
...
Eigenschaften:
- Sichtbar
...
InteraktivesObjekt
Methoden:
- Verschieben
...
Bitmap
Methoden: Eigen- Zeichnen schaften:
- Breite
...
- Höhe
...
Punkt
Methoden: Eigen- Zeichnen schaften:
- x
...
- y
...
Eigenschaften:
- Aktiv
...
Linie
Methoden: Eigen- Zeichnen schaften:
- APunkt
...
- Epunkt
...
Kreis
Methoden:
- Zeichnen
...
Eigenschaften:
- MPunkt
- Radius
...
Abb. 1.8: Ergebnisse der OOA für das Beispiel „Grafischer Editor“ (Freie Notation)
Nach der ersten groben Durchführung der objektorientierten Analyse wird das Modell
im nachfolgenden objektorientierten Design um zum Beispiel das physikalische
Modell erweitert.
1.3.2 Objektorientiertes Design (OOD)
Das objektorientierte Design wird oft auch als Zwischenschritt zwischen der Analyse
und der Implementierung verstanden. In der Phase des objektorientierten Designs
wird das in der objektorientierten Analyse gefundene Modell in Hinblick auf die
Implementierung untersucht und eventuell erweitert bzw. geändert. Wie schon
erwähnt, sollen beide Phasen OOA und OOD unabhängig von der für die
18
OOM/OOP
Implementierung verwendeten Programmiersprache (und auch Compiler) sein. Dies
kann aber zu Problemen führen, wenn zum Beispiel die gewählte
Programmiersprache keine Mehrfachvererbung unterstützt. Aus diesem Grunde
sollte man schon in der Designphase festlegen, welche Sprache zum Einsatz kommt,
um dann sprachenspezifische Eigenschaften im Modell einzuarbeiten. Unterstützt
zum Beispiel die Sprache oder der gewählte Compiler keine Mehrfachvererbung, so
muss diese durch Einfachvererbung ersetzt werden.
Weitere Entscheidungen, die in der Designphase geklärt werden bzw. das endgültige
Modell beeinflussen, sind die Wahl der konkreten Datenstrukturen für die Verwaltung
der Objekte. Für welche Beziehungen werden zum Beispiel Listen und für welche
Felder verwendet?
Abb. 1.9: Objektorientiertes Design
Auch das Bilden von logischen und physikalischen Einheiten sind Ergebnisse dieser
Phase der objektorientierten Modellbildung. Gerade die Aufteilung in eine
Modularchitektur ist für die spätere Implementierung, Wartung und Dokumentation
sehr wichtig.
Weitere Designentscheidungen sind die Wahl des Betriebssystems und damit das
Festlegen der Anwenderschnittstelle bzw. Benutzeroberfläche (Graphical User
Interface), die Art der Datenspeicherung und die Verwendung von
Klassenbibliotheken.
OOD des Beispiels „Grafischer Editor“:
Hier soll als Beispiel die Wahl der Datenstruktur und das Aufteilen in Module gezeigt
werden:
•
Wahl der Datenstrukturen
Für die Verwaltung der grafischen Objekte innerhalb einer Zeichnung wird eine
doppelt verkettete Liste verwendet, da die Anzahl der Objekte nicht festgelegt ist
und doppelte Verkettung eine bessere Performance bei Suchaktionen usw. bietet.
OOM/OOP
•
19
Physikalisches Modell (Modularchitektur)
Alle Klassen der Kategorie (Gruppe) Grafische Objekte werden in einem Modul
„gobjekt.cpp“ definiert.
Die Ergebnisse der OOD werden später mit den entsprechenden Notationen noch
einmal grafisch dargestellt.
1.3.3 Objektorientierte Programmierung (OOP)
In der Phase der objektorientierten Programmierung wird das entwickelte Modell in
einer Programmiersprache (vgl. Kapitel 2 „Objektorientierte Programmiersprachen“)
implementiert und zu einem Programm übersetzt.
Die Wahl der Sprache ist abhängig von den vorgenannten Entscheidungen innerhalb
des objektorientierten Designs.
Zur Zeit hat sich C++ als die meist verwendete und am weitesten verbreitete
objektorientierte Programmiersprache herausgestellt. Auch für die Implementierung
des Prototypen im Rahmen dieser Arbeit wurde C++ eingesetzt.
Als Beispiel für die Implementierung einer Klasse in C++ soll die Definition der
Klasse Kreis in Abb. 1.4 dienen.
1.4
Objektorientierte Modellierungstechniken
Alle objektorientierten Entwurfsprozesse sollen die durchgängige Softwareentwicklung von der Analyse über das Design zur Programmierung ermöglichen.
Gleichzeitig soll mit Hilfe spezieller Notationen auch die Dokumentation der
entwickelten Software damit erstellt werden können. Einige der Methoden werden
durch CASE-Tools unterstützt, die die Eingabe eines Modells grafisch interaktiv
unterstützen und daraus Programmcode in der gewählten Programmiersprache
erstellen. Auch das so genannte Reverse Engineering, also die Analyse von bereits
bestehendem Code und das Erstellen eines objektorientierten Modells, wird
unterstützt.
Die verschiedenen objektorientierten Entwurfsmethoden unterscheiden sich durch
ihre unterschiedliche Vorgehensweise bei der Entwicklung des Objektmodells und
durch die Unterstützung verschiedener Teilmodelle innerhalb eines objektorientierten
Gesamtmodells. Hier werden vor allem die logischen und physikalischen Sichtweisen
und die statischen und dynamischen Modelle eines Gesamtmodells unterschiedlich
gehandhabt.
Aus der Vielzahl dieser Methoden haben sich drei Verfahren herausgestellt, die in
der aktuellen Forschung (DFG-Schwerpunkt SP-694 „Objektorientierte Modellierung
in Planung und Konstruktion“) im Bauwesen angewendet werden und die zu den zur
Zeit am verbreitetsten und meistdiskutierten Verfahren der objektorientierten Welt
gehören. Die Konzepte, Vorgehensweisen und Notationen der Methoden von
Coad & Yourdon ([5],[6]), Rumbaugh et al [36] und Booch [3] sollen im
nachfolgenden kurz beschrieben und abschließend verglichen werden. Ein
ausführlicher Vergleich wird in [41] vorgestellt.
Im Kapitel 1.4 „Objektorientierte Modellierungstechniken“ dieser Arbeit soll deren
Einsatz für die Probleme des Bauwesens herausgestellt werden.
20
OOM/OOP
1.4.1 OOAD nach Coad & Yourdon
Die objektorientierte Analyse und Design Methode nach Coad & Yourdon ist in zwei
Büchern [5], [6] ausführlich beschrieben. Diese Methode zeichnet sich durch die
Einfachheit in ihrer Anwendung aus und wurde auch zum größten Teil bei der
Erstellung des objektorientierten CAD-Modells PreCAD verwendet.
Die objektorientierte Analyse (OOA) nach [5] gliedert sich in einzelne Teilaufgaben
und dient der Beschreibung des Objektmodells. Die OOA nach Coad & Yourdon
besteht aus nur einem einzigen Modell, dem OOA-Modell, das in der Analyse erstellt
und in dem darauf folgenden Design zum OOD-Modell erweitert wird. Hierbei wird für
beide Phasen die gleiche Notation verwendet.
Abb. 1.10: Das OOA-Modell nach Coad & Yourdon
Das OOA-Modell besteht aus fünf Schichten (Layer):
•
Fachgebiete-Schicht (Subject Layer)
•
Klassen & Objekt-Schicht (Class & Object Layer)
•
Struktur-Schicht (Structure Layer)
•
Attribut-Schicht (Attribute Layer)
•
Methoden-Schicht (Service Layer)
Diese Schichten werden nacheinander bearbeitet und können als Folien verstanden
werden, die beim Übereinanderlegen das Modell entsprechend beschreiben.
In einem ersten Schritt wird der Problemraum ("problem domain") genau
beschrieben, wobei dieser in einem quasi parallelen Schritt in eventuell vorhandene
Teilproblemräume unterteilt wird ("identifying subjects"). In den weiteren Schritten
werden die sich aus dem Problemraum ergebenden Objektklassen ("finding class &
objects") definiert und die Struktur der Klassenhierarchie ("identifying structures")
festgelegt bzw. modelliert. Hierbei werden mit Hilfe der Generalisierung gemeinsame
Teile von Objektklassen in einer Oberklasse zusammengefasst oder Objektklassen
durch weitere Verfeinerung gebildet ("gen-spec-structure"). Eine weitere Form der
Strukturierung ist die "whole-part"-Beziehung (Aggregation) zwischen Objektklassen,
d.h. Objekt A besteht aus Teilobjekt(en) der Klasse B. Nach der Formulierung der
Struktur werden die Eigenschaften ("defining attributes") und die Methoden
("defining services") der gefundenen Objektklassen festgelegt. Zu den Attributen
einer Klasse gehören auch die sogenannten "instance connections" (entspricht einer
normalen Assoziation) und die Nachrichtenverbindungen ("message connections").
OOM/OOP
21
Das OOD-Modell besteht aus vier Komponenten:
•
Problembereichs-Komponente (Problem Domain Component, PDC)
Erweiterung des OOA-Modells um implementierungsspezifische Informationen.
•
Benutzer-Komponente (Human Interaction Component, HIC)
Die Benutzerkomponente beschreibt die Anwenderschnittstelle des Systems.
•
Prozessmanagement-Komponente (Task Management Component, TMC)
Hier sollen die unterschiedlichen Prozesse in z.B. Multitaskingsystemen modelliert
werden.
•
Datenmanagement-Komponente (Data Management Component, DMC)
Hier wird das Speichern der Objekte im Modell realisiert.
BenutzerKomponente
ProblembereichsKomponente
ProzeßDatenmanagement- managementKomponente Komponente
Fachgebiete-Schicht
Klassen & Objekt-Schicht
Struktur-Schicht
Attribut-Schicht
Methoden-Schicht
Abb. 1.11: Die vier Komponenten des OOD-Modells
Die Vorgehensweise zur Erstellung der einzelnen Schichten des OOA-Modells
werden von den Autoren sehr ausführlich beschrieben und basieren vorwiegend auf
Heuristiken (z.B. das Auffinden von Klassen & Objekten).
Die Darstellung der einzelnen Layer erfolgt mittels einer speziellen grafischen
Notation.
22
OOM/OOP
Beispiel „Grafischer Editor“:
OOA:
Abb. 1.12: OOA nach Coad & Yourdon: Subject und Class & Object layer
Abb. 1.13: OOA nach Coad & Yourdon: Subject, Class&Object und Structure layer
OOM/OOP
Abb. 1.14: OOA nach Coad & Yourdon: Subject, C & O, Structure und Attribute layer
Abb. 1.15: OOA nach Coad & Yourdon: Subject, C & O, Structure und Service layer.
23
24
OOM/OOP
OOD:
Abb. 1.16: OOD nach Coad & Yourdon: HIC und DMC.
Beispielhaft
sind
hier
die
Benutzerkomponente
(HIC)
und
die
Datenmanagementkomponente (DMC) dargestellt, wobei nur die Klassen und
Objekte des aus der OOA stammenden Subjects dargestellt sind, mit denen eine
Beziehung besteht.
1.4.2 OMT nach Rumbaugh et al
Die „Object Modeling Technique“ (OMT) nach J. Rumbaugh und anderen [36]
beschreibt ein objektorientiertes Modell mit drei Teilmodellen:
•
Objektmodell
Das Objektmodell beschreibt die statische Struktur der Klassen und Objekte eines
Systems. Zur Darstellung des Objektmodells gibt es zwei Diagrammarten: das
Klassendiagramm und das Instanzendiagramm (dynamische Sicht als
Momentaufnahme zur Laufzeit).
•
Dynamikmodell
Das Dynamikmodell beschreibt die Aspekte eines Systems, die mit dem zeitlichen
Ablauf von Funktionen zu tun haben, wobei der Inhalt der Funktionen nicht
berücksichtigt wird. Zur Darstellung werden Zustandsdiagramme für Klassen
erstellt, die die zeitlichen Zustände von Klassen bzw. Objekten darstellen.
•
Funktionsmodell
OOM/OOP
25
Das Funktionsmodell beschreibt die Funktionalität, also funktionale Abhängigkeiten
eines
Systems.
Zur
Darstellung
eines
Funktionsmodells
werden
Datenflussdiagramme vorgeschlagen, die nicht besonders für objektorientierte
Systeme geeignet sind.
Durch das Vorhandensein des Dynamikmodells kann auch die Nebenläufigkeit von
Prozessen beschrieben werden.
Innerhalb der einzelnen Modellierungsphasen unterscheidet die OMT auch die
Analyse und Designphase. In der Analysephase werden die drei vorgenannten
Teilmodelle Objektmodell, Dynamikmodell und das Funktionsmodell mit Hilfe
spezieller Vorgehensweisen (siehe [36]) erstellt. Die Designphase unterteilt sich in
das Systemdesign, welches insbesondere die Systemarchitektur beschreibt und das
Objektdesign, welche das in der Analyse gefundene Modell im Hinblick auf die
Implementierung weiter verfeinert.
Beispiel „Grafischer Editor“:
OOA (Objektmodell):
Grafischer Editor
Anzahl Objekte
: Bitmap
: Grafisches Objekt
Neuzeichnen( )
Drucken( )
Selektieren( )
1
Zeichnung
Name
Aktiv
GibName( )
0..n
1
Grafisches Objekt
Gehört zu
Sichtbar
Zeichnen( )
Interaktives
Aktiv
Zeichnen( )
1
Bitmap
Breite
Höhe
Linie
: Punkt
Zeichnen( )
Zeichnen( )
Punkt
2
1
x
y
Kreis
1
Radius
: Punkt
1
Zeichnen( )
Abb. 1.17: OOA (Objektmodell) nach Rumbaugh et al.
Zeichnen( )
26
OOM/OOP
1.4.3 OOAD nach Booch
In der Methode von Booch „Object-Oriented Analysis and Design“ (vgl. [3]) werden
die logischen und physikalischen Sichten auf ein System mit der statischen und
dynamischen Semantik kombiniert und als folgende Teilmodelle (von Booch als
Diagramme bezeichnet) definiert:
•
Logische Struktur
•
Klassendiagramm
Klassendiagramme beschreiben die Klassen und ihre Beziehungen aus der
logischen Sicht. Klassendiagramme sind statisch und beschreiben die
Abstraktionen des Systems.
•
Objektdiagramm
Objektdiagramme beschreiben die Existenz von Objekten und deren
Beziehungen aus der logischen Sicht. Objektdiagramme sind dynamisch
und stellen eine Momentaufnahme zu einem speziellen Zeitpunkt dar.
Objektdiagramme zeigen nicht die zeitliche Abfolge von Ereignissen auf und
werden deshalb durch Interaktionsdiagramme ergänzt.
•
Physikalische Struktur
•
Moduldiagramm
Moduldiagramme zeigen die Zuordnung von Klassen zu Programmmodulen,
also die Software-Modularchitektur. Abhängigkeiten (Modul A abhängig von
Modul B) zwischen Modulen können auch angegeben werden.
•
Prozessdiagramm
Prozessdiagramme werden verwendet, um Systeme mit verteilten
Prozessen modellieren zu können. Dieses Diagramm enthält die
Prozessarchitektur des Systems.
•
Dynamische Sicht
•
Zustandsdiagramm
Booch: „Ein Zustandsdiagramm wird verwendet, um den Statusraum einer
bestimmten Klasse, die Ereignisse, die eine Statusänderung bewirken, und
die Aktionen, die aus einer Statusänderung resultieren, zu zeigen."
•
Interaktionsdiagramm
Interaktionsdiagramme stellen die Informationen der Objektdiagramme
anders dar. Sie zeigen ebenfalls die Objekte und Nachrichten, wobei durch
eine Zeitachse auch die zeitlichen Abfolgen von Ereignissen dargestellt
werden können.
Booch unterscheidet zwischen zwei Arten von Prozessen, dem Makro- und dem
Mikroprozess, die sich über die Analyse und Design Phasen erstrecken.
OOM/OOP
•
•
27
Makro-Prozess
•
Festlegen der grundsätzlichen Anforderungen (Konzeptualisierung)
•
Entwicklung eines Modells des gewünschten Verhaltens (Analyse)
•
Erzeugen der Architektur (Design)
•
Entwickeln der Implementation (Evolution)
•
Verwaltung der Evolution nach der Auslieferung (Wartung)
Mikro-Prozess
•
Festlegen der Klassen und Objekte auf einer bestimmten Abstraktionsebene
•
Festlegen der Semantik dieser Klassen und Objekte
•
Festlegen der Beziehungen zwischen diesen Klassen und Objekten
•
Spezifizieren der Schnittstelle und Implementation dieser Klassen und
Objekte
Beispiel „Grafischer Editor“:
Hauptelemente
Grafischer Editor
Zeichnung
Grafische Objekte
Grafisches Objekt
Interaktives Objekt
Bitmap
Kreis
Punkt
Linie
Abb. 1.18: OOA nach Booch, Kategorien
28
OOM/OOP
Grafischer Editor
Anzahl Objekte
: Bitmap
: Grafisches Objekt
Neuzeichnen( )
Drucken( )
Selektieren( )
Zeichnung
Name
Aktiv
GibName( )
0..n
1
1
Grafisches
Objekt
Gehört zu
Sichtbar
Zeichnen( )
A
Interaktives
Objekt
Aktiv
Zeichnen( )
A
1
Bitmap
Linie
Breite
Höhe
Zeichnen( )
: Punkt
Zeichnen( )
Punkt
2 1
x
1
y
Zeichnen( )
Kreis
1
Radius
: Punkt
Zeichnen( )
Abb. 1.19: OOA nach Booch, Klassendiagramm
1.4.4 Vergleich der Verfahren
Wie in der Beschreibung der einzelnen Methoden schon öfters erwähnt wurde, sind
die wichtigsten Eigenschaften einer Methode zur objektorientierten Modellierung von
Softwaresystemen die Unterstützung der logischen und physikalischen Struktur und
der statischen und dynamischen Merkmale. Die wichtigsten Merkmale eines
objektorientierten Modells sind die Klassen und deren Beziehungen untereinander.
Hier sollten die diversen Entwurfsmethoden entsprechende Konzepte (Arten von
Beziehungen), Prozesse („Wie finde ich die Klassen und Objekte meines
Problemraumes“) und Notationen (Symbole zur Darstellung des Modells) zur
Verfügung stellen. Die vorgestellten Entwurfsmethoden verfolgen unterschiedliche
Ziele und Konzepte für die Modellbildung und sind aus diesem Grunde für
unterschiedliche Aufgabenstellungen geeignet.
OOM/OOP
29
Betrachtet man die logischen Modelle, so ist die Darstellung des Objektmodells mit
Hilfe von Klassendiagrammen in allen Methoden vom Prinzip her gleich. Hiermit wird
mit Hilfe der unterschiedlichen Symbolik das statische Modell beschrieben. Bei der
Erzeugung der Klassendiagramme gibt es unterschiedliche Herangehensweisen
innerhalb der Analyse und Design Phasen, welche mehr oder weniger ausführlich
beschrieben sind. Während Rumbaugh seine drei Teilmodelle (Objekt-, Funktionsund Dynamikmodell) voneinander abgrenzt, sind bei den Anderen fließende
Übergänge zwischen den Teilmodellen vorhanden. Das Funktionsmodell von
Rumbaugh verlangt eine Trennung von Daten und Funktionen, was dem wichtigsten
Prinzip der Objektorientierung widerspricht.
Das dynamische Modell wird bei Booch mit Hilfe von Objekt- und
Interaktionsdiagrammen beschrieben, welche die dynamischen Abläufe und den
Nachrichtenaustausch zwischen den Objekten beinhalten. Coad & Yourdon
beschreiben dynamische Zusammenhänge im OOA-Modell mittels einzelnen
Schichten (z.B. Methodenschicht). Rumbaugh wiederum verwendet das Event Trace
Diagramm zum Beschreiben der zwischen den einzelnen Objekten auftretenden
Nachrichten.
Was die physikalischen Modelle betrifft, so werden diese zwar von Rumbaugh und
Coad & Yourdon vorgeschlagen, allerdings ohne konkrete Konzepte und Notationen
für deren Modellierung. Im Gegensatz zu Booch, der die Aspekte der physikalischen
Struktur sehr genau beschreibt und somit eine durchgängige Systementwicklung bis
zur Implementierung zulässt.
externe Modellierung
zwischen Objekten & Klassen
Rumbaugh
Coad & Yourdon
Booch
statisch
Objektmodell
Funktionsmodell
OOA – Modell
(verteilt über alle Schichten)
Klassendiagramm
dynamisch
Event Trace
OOA - Modell
(Methoden- und Attribut-Schicht)
Objektdiagramm
Interaktionsdiagramm
Abb. 1.20: Modellierung des Objektmodells
Ein weiteres Merkmal ist die jeweilige Unterstützung von Klassen. Gemeint sind hier
spezielle Klassen wie generische Klassen (in C++ Templates genannt) und
Metaklassen (in C++ nicht vorhanden). Nicht jede Methode unterstützt diese
allerdings programmiersprachenspezifischen Klassen (vgl. Abb. 1.21).
30
OOM/OOP
Typen von Klassen
konkrete
Klassen
abstrakte
Klassen
•
•
•
•
•
•
generische
Klassen
Metaklassen
•
•
Rumbaugh et al
Coad & Yourdon
Booch
Abb. 1.21: Verschiedene Typen von Klassen
Was die Modellierung der Beziehungen zwischen Klassen und Objekten betrifft, so
gibt es dort die in Abb. 1.22 aufgeführten Arten von Beziehungen, welche nur von
Booch vollständig unterstützt werden.
Beziehungen zwischen Klassen
Verwendungs
-beziehung
Assoziation
Aggregation
•
•
•
•
•
•
•
•
Metaklassenbeziehung
Instanziierung
•
•
Rumbaugh et al
Coad & Yourdon
Booch
Abb. 1.22: Verschiedene Typen von Beziehungen zwischen Klassen
Die Notationen unterscheiden sich durch die Verwendung unterschiedlicher
Symbole, welche von Hand schwierig zu zeichnen sind. Außer den Symbolen von
Rumbaugh, sind alle anderen ohne eine Toolunterstützung nur schwierig zu
verwenden (Abb. 1.23).
Notation
von Hand
zeichenbar
Anzahl der
Symbole
Rumbaugh et al
leicht
ca. 20
Coad & Yourdon
bedingt
ca. 10
Booch
bedingt
ca. 35
Formale
Beschreibung
Data
Dictionary
Klassenspezifikation
verschiedene
Spezifikationen
Abb. 1.23: Notationen im Vergleich
In Abb. 1.24 sind die Symbole der wichtigsten objektorientierten Entwurfsmethoden
als Übersicht zusammengestellt.
OOM/OOP
31
Abb. 1.24: Verschiedene Notationen objektorientierter Entwurfsmethoden
Der Analyse- und Designprozess ist in jeder Methode anders gestaltet und auch von
unterschiedlicher Qualität. Hauptsächlich werden Vorgehensweisen vorgeschlagen,
um systematisch Klassen & Objekte des Problemraumes und deren Beziehungen,
Attribute und Methoden zu finden. Hierbei geht der Weg (vgl. Abb. 1.25) von der
grammatikalischen Untersuchung des Pflichtenheftes (Rumbaugh) bis hin zu
Heuristiken (Coad & Yourdon).
32
OOM/OOP
Vorgehensweise bei der Entwicklung
Auffinden von
Klassen und
Eigenschaften
Beschreibung
des
Analyseprozesses
Beschreibung
des
Designprozesses
Rumbaugh et al
grammatikalische
Untersuchung
ausführlich
ausführlich
Coad & Yourdon
Heuristiken
ausführlich
knapp
Heuristiken,
Use-Cases,
Object Behavior
ausführlich
ausführlich
Booch
Abb. 1.25: Analyse- und Designprozess der Methoden
Betrachtet man die vorgestellten Entwurfsmethoden, so ist die neueste, die Methode
nach Booch, wohl auch die am umfangsreichsten und für alle Arten von
Anwendungen am geeignetsten. Booch vereint viele Konzepte und Entwurfsprozesse
aus den beiden anderen Methoden (Rumbaugh, Coad & Yourdon). Diese Methode
ist sehr komplex und dadurch weniger für kleinere Systeme geeignet.
Die Methode nach Coad & Yourdon ist schnell und leicht erlernbar und hat ihre
Vorteile in ihrer ausführlichen Beschreibung, zumindest was die Analyse betrifft.
Gerade die ausführliche Beschreibung von Vorgehensweisen zum Auffinden und
Verifizieren von Klassen & Objekten für einen speziellen Anwendungsbereich ist eine
gute Hilfe bei der objektorientierten Analyse. Schwachpunkte sind das Fehlen von
Konzepten zur Modellierung komplexerer Strukturen, wie zum Beispiel generische
Klassen.
Die OMT nach J. Rumbaugh et al wird von Schäfer [41] als „verwirrend“ bezeichnet.
Gerade die Trennung in Objektmodell und Funktionsmodell, was ja dem wichtigsten
Paradigma der Objektorientierung widerspricht, dient nicht gerade dem Verständnis
dieser Methode.
In der vorliegenden Arbeit wurde die Methode von Coad & Yourdon zur Modellierung
des Kernmodells, nämlich dem zentralen Objektmodell, eingesetzt. Nach Bekannt
werden der Booch Methode wurde diese für die Modellierung der beiden Teilmodelle
Kostenmodell und Wohnflächenermittlung angewendet und führte auch zu guten
Ergebnissen.
Ein ausführlicher Vergleich ist in Schäfer [41] enthalten.
OOM/OOP
33
2 Objektorientierte Programmiersprachen
Objektorientierte Programmiersprachen dienen der Umsetzung des entwickelten
Modells in eine Implementierungssprache.
Als objektorientierte Programmiersprachen sollen hier
•
•
•
•
•
•
•
Smalltalk
Eiffel
CLOS
Ada
Object Pascal
C++
Java
genannt werden.
Eine Kurzbeschreibung der vorgenannten Programmiersprachen ist z.B. in Booch [3]
und
in
Heuer
[19]
enthalten.
Ausführlichere
Beschreibungen
der
Programmiersprache C++ geben z. B. Jell/Reeken [22] und Vetter [45].
Die zur Zeit am meisten eingesetzte und am weitesten verbreitete objektorientierte
Programmiersprache ist C++. C++ wurde von Bjarne Stroustroup [44] entwickelt, um
„einfacher“ Programmieren zu können. C++ beinhaltet fast alle Prinzipien und
Elemente der Objektorientierung außer der Persistenz und den Metaklassen.
Um Objektmodelle persistent speichern zu können, werden zurzeit objektorientierte
Datenbanksysteme entwickelt (vgl. hierzu [19], [33]).
Der zu dieser Arbeit entwickelte Prototyp ist in der objektorientierten
Programmiersprache C++ implementiert worden.
2.1
Objektorientierte Modellierung im Bauwesen
Der Einzug der objektorientierten Modellierung im Bauwesen kann in Deutschland
mit dem Beginn des DFG Schwerpunktprogramms SP-694 „Objektorientierte
Modellierung in Planung und Konstruktion“, das 1991 begonnen hatte, gleichgesetzt
werden. An diesem Schwerpunkt sind fast alle Bauinformatik-Lehrstühle
Deutschlands mit verschiedenen Teilprojekten unterschiedlichster Themen beteiligt.
Eine Übersicht über die einzelnen Teilprojekte wird im Arbeitsbericht zum o.g.
Schwerpunktprogramm gegeben.
Um einen Überblick über die objektorientierte Modellierung im Bauwesen zu geben,
sollen im Folgenden einige, zum Zeitpunkt der Erstellung dieser Arbeit noch
laufende, Teilprojekte kurz vorgestellt werden. Fast alle Projekte arbeiten mit der
OMT von Rumbaugh als Entwurfsmethode und die Prototypen sind fast alle in C++
auf den unterschiedlichsten Plattformen implementiert.
Ein Projekt, das sich mit der „Modellierung und Entwicklung eines wissensbasierten,
objektorientierten Systems zur Planung von optimierten Baustellen-Layouts“
beschäftigt, arbeitet als Einziges mit der objektorientierten Programmiersprache Eiffel
[25], [26].
Im Bereich der Planungs- und Produktionsprozesse beschäftigt man sich mit
objektorientierter Gittermodellierung geotechnischer Systeme [7] und mit
objektorientierten Teilproduktmodellen für die Systemintegration von Planungs- und
34
OOM/OOP
Konstruktionsvorgängen im Bauwesen [37], [38], [40]. Im Bereich des Grundbaus
arbeitet man an „Objektorientierten Modellen für herstellungsgerechte Konstruktion
im Grundbau“ [20]. Auch wissensbasierte Ansätze werden in verschiedenen
Projekten verfolgt, wie z.B. im Teilprojekt „Wissensbasierte Systeme zur
Unterstützung von Entwurfs- und Konstruktionsprozessen im Bauwesen auf der
Grundlage objektorientierter Modellierung“.
Eines der Hauptziele des o.g. Schwerpunktes ist es, durch die Anwendung der
verschiedenen objektorientierten Entwurfsmethoden auf die unterschiedlichsten
Themengebiete aus dem Bereich des Bauwesens deren Tauglichkeit zu prüfen, und
entsprechende Erfahrungen und Wissen auf dem Gebiet der objektorientierten
Modellierung zu sammeln.
Nach dem Abschluss dieses Schwerpunktprogramms (Mitte 1998) kann also die
objektorientierte Methode aus der Sicht des Bauwesens entsprechend
wissenschaftlich bewertet werden.
Ob und welchen Einfluss die Anwendung der objektorientierten Methode bei der
Softwareentwicklung innerhalb des Bauwesens hat, kann dann zu diesem Zeitpunkt,
an Hand von vielen Beispielen, wissenschaftlich fundiert, aufgezeigt werden.
OOM/OOP
3 Die Programmiersprache C / C++
3.1
Allgemeines
•
Entstehung
o Die Sprache C entstand ca. 1970 durch die Entwicklung des
Betriebssystems UNIX bei Bell Telephone Laboratories Inc.
•
Definition
o Durch das Buch "The C Programming Language" der Bell UNIXEntwickler B. Kernigham und D. Ritchie im Jahre 1978
•
Normung
o ANSI X3.159 im Jahre 1989, American National Standard Institute,
Language C, Bezeichnung ANSI-C
•
Standards
o Durch Definition von Kernigham und Ritchie, auf UNIX-Rechnern
verbreitet und mit K&R-C bezeichnet. Entsprechend der Normung
ANSI-C. Beinhaltet K&R-Definition.
•
Literatur
o Günther Lamprecht
C, Einführung in die Programmiersprache
Vieweg 1986
ISBN 3-523-03362-2
o Kernigham/Ritchie
Programmieren in C
Carl Hanser Verlag 1983 München, Wien
ISBN 3-446-13878-1
o Claus Schirmer
Die Programmiersprache C
Die ausführliche Beschreibung aller Sprachelemente
3., völlig neubearbeitete Auflage ANSI C
Carl Hanser Verlag 1992 München, Wien
35
36
3.2
OOM/OOP
Erstes C++ -Programm
Ein erstes Programm soll die Worte ausgeben:
hello, world
Im MS Visual Studio erfolgt dies in den folgenden Schritten:
1. Starten des VS
2. Datei Ö Neu
3. Im Reiter Projekte Konsolenanwendung auswählen, Projektname eintragen.
OOM/OOP
4. Ok Drücken, im nächsten Dialog „Eine Hello world Anwendung“ auswählen
5. Links im Visual Studio kann man unter
den Quellcode des Programmes auswählen:
// Hello World.cpp: Definiert den Einsprungpunkt für die
Konsolenanwendung.
//
#include "stdafx.h"
#include "iostream.h"
int main(int argc, char* argv[])
{
cout << "Hallo Welt!\n";
return 0;
}
6. Bevor das Programm gestartet werden kann, muß es kompiliert werden.
Hierfür die Tasten STRG + F5 drücken.
37
38
3.3
OOM/OOP
Programmaufbau
Ein C-Programm besteht aus
•
•
•
•
Definitionen und Deklarationen von Variablen und Funktionen
Präprozessor-Anweisungen
genau eine Funktion muss mit main bezeichnet sein
Funktionen können in Dateien verteilt sein
OOM/OOP
39
Fehlerkorrektur
Editor
z. Bsp. Basic
Quellenprogramm
Ausführung durch
interpretieren
Bibliotheken
Compiler
Fehlerkorrektur
Objektprogramm
Bibliotheken
Binder
Fehlerkorrektur
Programm als
Lademodul
z. Bsp. C++, Fortran
Programmerstellung
geladenes
Programm
Ausführung
Programmausführung
Bild: Erstellung von ablauffähigen Anwendungsprogrammen
Fehlerkorrektur
40
OOM/OOP
Starten eines Programms
•
•
Die Funktion main wird implizit als erste Funktion aufgerufen.
In einer Betriebssystemumgebung wie DOS oder UNIX werden der mainFunktion Programmparameter und Environment-Variablen übergeben.
Beenden eines Programms
•
•
Beendung des zu der Funktion main gehörenden Anweisungsblockes oder mit
return-Anweisung in main oder Aufruf von exit an einer beliebigen Stelle.
Laufzeitfehler im Programm (beispielsweise Division durch Null)
#include <stdio.h>
#include <stdlib.h>
Präprozessoranweisungen
extern int Funktion();
Deklaration externer Funktion
main(char **argv, int argc)
{
int i = 1;
Hauptprogramm,
Programmparameter
Funktion();
Beendung mit exit
exit(0);
return 0;
oder mit return
}
3.4
Interne Darstellung von Werten
Zeichen, Zeichenketten und Zahlen werden im Rechner in Binärcode dargestellt.
Durch eine begrenzte Verwendung von Bits (Binärziffern) ist nur ein begrenzter
Wertebereich darstellbar.
3.4.1 Zeichen
Zeichen (char) werden in der Regel mit einem 8 Bit Code dargestellt
(Rechnerabhängig werden auch 7 oder 9 Bits verwendet). Mit einem Byte stellen sie
die kleinste Werteinheit in C++ dar. Eine Ordnung der Zeichen ist gegeben durch den
EBCDIC-Code
Extended Binary Code Decimal Interchange Code
oder
ASCII-Code
American Standard Code for Information Interchange
OOM/OOP
41
Diese Ordnungssysteme bestimmen die Wertigkeit der Zeichen.
Mit 8 Bits sind 28 = 256 verschiedene Zeichen (Buchstaben, Ziffern, Sonderzeichen)
darstellbar.
Beispiel: ASCII-Ordnungswerte
Zeichen ´A´ = Bitmuster mit Wert 65
Zeichen ´0´ = Bitmuster mit Wert 48
3.4.2 Ganzzahlen
Ganzzahlen (Integer) werden mit 16 Bit (short int) oder mit 32 Bit (long int)
gespeichert. Das signifikante Bit (Bit 0) wird als Vorzeichen-Bit benutzt.
Somit können 232 verschiedene Zahlen (positiv und negativ) dargestellt werden.
-2147483648
<= Wert <=
+2147483647
-231
<= Wert <=
+231
3.4.3 Gleitkomma-Zahlen
Gleitkomma-Zahlen werden mit 4 Byte (float) oder 8 Byte (double) gespeichert.
In der Regel werden float und double Zahlen nach dem Standard
ANSI-IEEE 754-1985 aufgebaut.
Beispiel: float-Wert mit 4 Byte
42
OOM/OOP
Wert = (Vorzeichen)(Mantisse x 2) (Exponent - 127)
8.43 -37
<= float-Wert <=
3.37 38
Bei 8 Byte double Wert:
4.19-307
<= double-Wert <= 1.67 308
3.4.4 Zeichenketten
Zeichenketten (Literale, Strings) werden in C++ als eine Reihe von Zeichen (char)
dargestellt. Das Ende einer Zeichenkette ist durch das ASCII-Zeichen NULL
(Zeichen mit dem Wert 0) festgelegt.
Die Zeichenkette "Hallo Welt" besteht aus den 10 Zeichen mit den Werten der
Buchstaben und endet mit dem Byte NULL.
3.5
Sprachsymbole
Ein C++ Programm besteht aus einzelnen Symbolen der 6 Wortklassen:
•
•
•
•
•
•
Namen (Bezeichner)
Reservierte Wörter
Numerische Konstanten
Literale
Operatoren
Trenner
OOM/OOP
43
3.5.1 Namen (Bezeichner)
Eine Folge von Buchstaben (a-z, A-Z) und Ziffern (0-9) sowie dem Unterstrich.
Sonderzeichen und Umlaute sind nicht erlaubt. Ein Name (Bezeichner) muss mit
einem Buchstaben beginnen, es wird zwischen Groß- und Kleinschreibung
unterschieden. Die Länge eines Namens ist von der Implementierung des CCompilers abhängig (meist <= 31). Signifikant sind für externe Namen meist die 6
ersten Zeichen eines Namens.
Beispiel: Variable1, variable1, Anzahl_Werte
(Microsoft VC++: 247 Zeichen)
3.5.2 Reservierte Wörter
Reservierte Wörter sind Schlüsselwörter für die Konstrukte der Sprache C++.Sie
dürfen nicht als Bezeichner verwendet werden.
abstract
boolean
const
else
final
implements
multicast
protected
sizeof
this
typedef
auto
break
continue
entry
finally
import
native
public
static
throw
union
asm
case
default
enum
float
int
new
register
struct
throws
unsigned
break
catch
delegate
extern
for
instanceof
null
return
super
transient
void
byte
char
do
extends
goto
interface
package
short
switchtrue
true
volatile
bool
class
double
false
if
long
private
signed
synchronized
try
while
3.5.3 Numerische Konstanten
Als Konstanten sind Ganzzahl-, Gleitkommawerte und Zeichen vorhanden.
3.5.4 Ganzzahlige Konstanten
In C++ besteht die Möglichkeit, Konstanten zu verschiedenen Basissystemen zu
definieren.
Dezimal:
Oktal:
Hexadezimal:
Eine Kette von Ziffern von 0 bis 9 und Vorzeichen
Beispiele: 1, 2, +199, 32767, -17
Eine Kette von Ziffern, welche zur Basis 8 interpretiert werden, wenn sie mit der Null beginnen.
Beispiele: 03, 007, 0177
Eine Kette von Ziffern beginnend mit 0x oder 0X
wird zur Basis 16 interpretiert.
Beispiele: 0x0, 0x100, 0X444
44
OOM/OOP
int-Konstanten:
Ganzzahlkonstanten werden als 16/32-Bit int-Werte (16-Bit Wertebereich von
+32767 bis -32768, 32-Bit siehe long-Konstante) dargestellt. Ist ein größerer
Wertebereich erforderlich, so kann dies explizit vereinbart werden mit longKonstanten.
long-Konstanten:
Die Kette von Ziffern wird mit einem l oder L abgeschlossen. Die Definition kann
hierbei dezimal, oktal oder hexadezimal erfolgen. Der Wertebereich liegt dann
entsprechend einer 32-Bit Darstellung bei +2147483647 bis -2147483648.
Beispiele: 100000L, 0x40000L.
char-Konstanten:
Ein einzelnes Zeichen kann durch die Angabe des Zeichens in Anführungszeichen,
also ´x´ angegeben werden. Der ganzzahlige Wert dieser Konstanten entspricht
dann dem Zeichensatz der Maschine. (Ordnung entsprechend ASCII, EBCDIC,
Wertebereich 7, 8 oder 9 Bit).
Um ein Bitmuster (Bit-Wert) exakt anzugeben, kann die Angabe von char-Konstanten
durch \ddd in oktaler Definition erfolgen. Besondere Zeichen wie Zeilentrenner \n
werden mit dem Fluchtsymbol \ dargestellt.
Beispiele:
´a´
Zeichen a
´1´
Zeichen 1
´\r´
Wagenrücklauf
´\n´ Zeilentrenner
´\0´ Bitmuster identisch Null (ASCII NUL)
´\040´ oder ´ ´ stellen in ASCII das Leerzeichen dar.
Gleitkomma-Konstanten
Eine Gleitkomma-Konstante besteht aus einem ganzzahligen Teil, einem
Dezimalpunkt, einem Dezimalbruch, dem Zeichen e oder E und einem ganzzahligen
Exponenten. Jede Konstante wird als 64-Bit double Wert dargestellt.
Beispiele: -0.1, .5, 1.3e17, 17.E+99
3.5.5 Literale (Zeichenketten)
Ein Literal wird durch eine Folge von Zeichen, in Hochkommas "Dies ist eine
Zeichenkette" eingeschlossen, gebildet. Es entspricht einer statischen Adresse auf
diese Zeichen, welche mit dem Bitmuster ´\0´ abgeschlossen werden. In C++ gibt es
keine Operatoren auf Literale. Alle Operationen müssen auf die einzelnen Zeichen
und das Endezeichen (Byte mit dem Wert Null ´\0´) erfolgen.
OOM/OOP
45
3.5.6 Trenner
Dienen dem Trennen der Wortsymbole untereinander. Möglich sind Leerzeichen,
Zeilentrenner und Tabulatoren sowie Kommentare.
Kommentare werden in die Zeichen /* und */ eingeschlossen und dürfen überall im
Programmtext vorkommen. Sie dürfen jedoch nicht verschachtelt werden.
// Dies ist ein einzeiliger Kommentar
/* Dies ist auch ein einzeiliger Kommentar */
/*
Dies ist
ein mehrzeiliger
Kommentar
*/
3.6
Datenobjekte und L-Werte
Ein Datenobjekt ist ein ansprechbarer Speicherbereich, in die eine CPU Werte
(Ganzzahlen, Gleitkommazahlen, Zeichen) ablegt. Ein Datenobjekt kann durch einen
sogenannten L-Wert (left hand value) angesprochen werden.
Im einfachsten Fall ist dies eine Variable.
3.6.1 Variable
Eine Variable ist der symbolische Name eines Speicherbereiches im Hauptspeicher.
In diesem Speicherbereich werden Werte (Daten) abgelegt. Die Manipulation der
Variablen (Datenobjekte) erfolgt mithilfe von Operatoren.
Beschrieben werden Variablen durch folgende Angaben:
Bezeichner
Variable
Pi
46
OOM/OOP
Datentyp
Wert
Speicheradresse
float
3.14159265…
0x1000:0x2A34
•
Der Bezeichner dient dem Identifizieren eines Datenobjektes in einem Programm.
Er muss einen gültigen Namen darstellen.
•
Eine Variable muss vor ihrer Benutzung deklariert werden.
•
Die Deklaration legt durch den Datentyp die interne Repräsentation (Darstellung
und Größe in Byte) des Datenobjektes und somit den Wertebereich fest. Dies
bedingt die möglichen Operationen auf Datenobjekte.
•
Der Wert einer Variablen kann durch eine Initialisierung beim Kompilieren oder zur
Laufzeit des Programms gebildet werden.
•
Die Speicheradresse bezeichnet die Adresse (Ort) eines Datenobjektes, an dem
sein Wert im Speicher des Rechners beginnt. Für Datenobjekte größer 1 Byte ist
dies die Anfangsadresse des Objektwertes.
3.6.2 Deklaration von Variablen
Vor der Benutzung einer Variablen müssen Vereinbarungen zu Speicherklasse und
Typ der Variablen vorgenommen werden.
Deklaration:
[Speicherklasse] Typspezifizierer Variablenliste [Initialisierer];
Wirkung: Ein (oder mehrere) Variable(n) werden mit ihrem
Datentyp und ihrer Speicherklasse festgelegt.
•
Speicherklassen können sein typedef, extern, static, auto und register.
•
Typspezifizierer sind void, bool, char, short, int, long, signed, unsigned,
const, volatile, float, double, struct, union, enum.
•
Variablenliste ist eine Liste gültiger Namen mit mindestens einem Namen.
•
Initialisierer sind Anfangswerte entsprechend den Typspezifizierern.
•
Im Regelfall wird bei einer Deklaration Speicherplatz belegt und man spricht von
einer Definition.
main()
{
int Var1;
double w1,w2,w3,w4=0.0;
OOM/OOP
47
long int Lval = 333;
}
3.6.3 Deklaration von Variablen
→ Variablen können an beliebiger Stelle in einem Block definiert werden.
void test(void)
{
for (int i = 1; i < 10; i++)
{
int j = i * 10;
…
double d;
}
}
3.6.4 Datentypen
Datenobjekte sind durch ihren Datentyp (Typspezifizierer) festgelegt. Der Typ
beschreibt den Inhalt und Wertebereich eines Objektes und somit die möglichen und
sinnvollen Operationen (mittels Operatoren) auf die Objekte. Aus den Grundtypen
lassen sich eigene Typen (typedef) zusammensetzen. Mit Subtypen wird der
Wertebereich eines Typs beschrieben.
3.6.5 Datentyp bool
Eine Variable vom Typ bool kann nur zwei Zustände, nämlich true und false
annehmen.
• Alle bedingten Ausdrücke geben einen bool-Wert zurück!!!
• Bei einer Typkonversion wird bei der Zuweisung an einen int
true als 1 und false als 0 konvertiert.
3.6.6 Datentyp int
•
Ganze Zahlen
•
Natürlicher Datentyp der CPU. Ein int-Datenobjekt hat die Größe eines CPUWortes und ist deshalb durch die Implementierung des C-Compilers bedingt.
48
OOM/OOP
int i = 1;
unsigned short int j = 17;
unsigned ui = 100;
long k = 0x400;
•
Genauigkeit der Subtypen
short int
16 Bit
int
16/32 Bit
long int
32 Bit
215 <= i <= 215-1
(Betriebssystemabhängig)
231 <= i <= 231-1
•
Bei Subtypen kann das Schlüsselwort int entfallen.
•
Durch unsigned kann ein int-Typ vorzeichenlos deklariert werden. Der
Wertebereich ist dann 2Anzahl_Bits .
3.6.7 Datentyp char
•
Eine Variable des Datentyps char stellt in C++ eine ganze Zahl dar. Sie besitzt
die Größe 1 Byte. Hiermit können 28 unterschiedliche Werte (Zeichen) dargestellt
werden.
•
Kann überall dort eingesetzt werden, wo ein int-Wert erwartet wird.
•
Ordnungsweise ist zumeist der ASCII-Zeichensatz.
•
Betriebssystemabhängig mit Vorzeichen behaftet, d.h. -27 <= c <= 27 - 1
•
Durch unsigned char wird der char-Typ vorzeichenlos.
•
Beispiel:
char Zeichen_A = ´A´;
3.6.8 Datentyp float
•
Gleitkomma Zahlen
•
Zwei
unterschiedliche
Genauigkeiten,
implementierungsabhängig.
exakte
Wertebereiche
float
32 Bit
ca. 7-8 Ziffern
+/- 10 +/- 37
double
64 Bit
ca. 14-16 Ziffern
+/- 10 +/- 307
(* Interne Darstellung z.B. entsprechend ANSI IEEE 754-1985 Standard.
Data Type Ranges in Visual C++)
3.7
Programmierstil
sind
Siehe
OOM/OOP
49
Der Stil der Schreibweise von Programmtext in der Programmiersprache C++
bestimmt wesentlich die Verständlichkeit des Programms.
Als formatfreie Programmiersprache erlaubt C++ eine übersichtliche Gestaltung des
Programmtextes. Dies lässt sich durch eine Anweisung pro Zeile und Einrücken der
Kontrollkonstrukte erreichen. Die nachfolgenden Beispiele sind beide syntaktisch
korrekt für einen Compiler, jedoch unterschiedlich lesbar für den Programmierer.
Schlechte Gestaltung:
main(){
int i,j;j = 0;for (i=0;i<10;i=i+1){j=j+i;if ((j
%2)!=0) cout << j;
}exit(0);}
Bessere Gestaltung:
main()
{
int i,j;
j = 0;
for (i=0; i<10; i=i+1)
{
j = j+i;
if ((j %2) != 0)
cout << j;
}
exit(0);
}
3.8
Formatierte Ein- und Ausgaben
In der Sprache C++ sind alle Funktionen der Ein- und Ausgabe von Tastatur, auf
dem Bildschirm und in Dateien in Bibliotheken enthalten. Zur Ein- und Ausgabe von
Daten wird von, bzw. aus, Zeichenströmen gelesen. Der Strom istream ist mit der
Tastatur und ostream mit der Konsole standardmäßig verbunden.
Um einen Text auszugeben, wird cout verwendet. cout ist eine Instanz der Klasse
ostream (output stream). Die Übergabe der auszugebenden Daten an cout erfolgt
mit Hilfe des <<-Operators. Die Daten werden sozusagen in den Ausgabestrom
geschoben. Hinter dem <<-Operator steht entweder eine Zeichenkette in Anführungszeichen (" ") oder eine Variable. Diese werden dann durch << von einander
getrennt. (siehe Beispiel weiter unten)
Für die Eingabe wird cin benötigt; eine Instanz der Klasse istream (input stream).
Da der Datenstrom diesmal von cin zu einer Variablen erfolgt, wird hierbei der >>Operator eingesetzt. (ein Beispiel folgt weiter unten). Sollen Werte für mehrere
Variablen eingelesen werden, so folgt vor jeder Variablen der >>-Operator. Bei der
Eingabe werden die Werte dann mit einer Leertaste zwischen den einzelnen Werten
eingegeben.
50
OOM/OOP
Um cin und cout im Programm verwenden zu können, muss die Header-Datei
iostream.h eingebunden werden.
#include <iostream>
Wenn mehr als eine Zeile ausgegeben werden soll, ist es erforderlich das Ende einer
Zeile kenntlich zu machen. Dazu gibt es zwei Möglichkeiten: einmal kann das
Steuerzeichen (Escape-Sequenz) \n in den Text geschrieben werden oder es wird
endl an cout übergeben.
cout << "Text in Zeile 1!\n";
cout << "Text in Zeile 2!" << endl;
endl ist eine Funktion, die ebenfalls das Steuerzeichen \n ausgibt, aber weiterhin
auch einen so genannten flush durchführt. Der flush dient dazu, den Inhalt des
Ausgabepuffers, in welchem zunächst eine bestimmte Anzahl von Zeichen gesammelt wird, auszugeben. Normalerweise wird der Puffer erst ausgegeben, wenn dieser
voll ist. Mit flush wird also eine vorzeitige Ausgabe bewirkt. Jede Zeile, bzw. bei
mehreren zusammenhängenden nur die letzte Zeile, muss mit endl oder flush
abgeschlossen werden, da sonst nicht zwangsläufig eine Ausgabe erfolgt. Es sei
denn, dass anschließend mit cin eine Eingabe folgt; dabei wird zuvor der Inhalt des
Ausgabepuffers ausgegeben.
Weitere Escape-Sequenzen wie \n sind in der nachstehenden Tabelle angegeben.
Escape-Sequenz
\a
\b
\f
\n
\r
\t
\v
\"
\'
\?
\\
Zeichen, bzw. Ausgabe
Akustisches Signal
Backspace (Cursor geht eine Position nach links)
Seitenvorschub
New Line (Cursor geht zum Anfang der nächsten Zeile)
Cursor geht zum Anfang der aktuellen Zeile
Cursor geht zur nächsten horizontalen Tabulatorposition
Cursor geht zur nächsten vertikalen Tabulatorposition
" wird ausgegeben
' wird ausgegeben
? wird ausgegeben
\ wird ausgegeben
#include <iostream>
main()
{
char buf[80];
int i;
OOM/OOP
51
double w;
/* Eingabe */
cout << "Eingabe einer Ganzzahl:" << endl;
cin >> i;
cout << "Eingabe einer Gleitkommazahl:" << endl;
cin >> w;
cout << "Eingabe einer Zeichenkette:" << endl;
cin >> buf;
/* Ausgabe */
cout << "Es wurde eingegeben: ";
cout << i << ", " << w << ", " << buf << endl;
}
Formatierte Ausgabe
Um die Ausgabe mit cout zu formatieren, existieren eine Reihe von Manipulatoren,
die in den folgenden Tabellen aufgelistet sind.
Ganzzahlen
Manipulator
setw(x)
Beschreibung
Gibt (nur) den darauffolgenden Wert mit der Feldbreite x aus. So
kann z.B. auch eine Tabelle ausgegeben werden.
Beispiel:
cout << setw(7) << 123 << setw(7) << -25 << endl;
setfill('*') Füllt Leerstellen mit bestimmtem Zeichen (z.B. *) aus.
left
Alle Zahlen werden linksbündig ausgegeben.
internal
Vorzeichen werden linksbündig und Zahlen rechtsbündig
ausgegeben.
right
Alle Zahlen werden rechtssbündig ausgegeben (Standard).
showpos
Stellt das positive Vorzeichen ebenfalls dar.
noshowpos
Hebt showpos wieder auf.
oct, dec,
Zahlen können in drei verschiedenen Zahlensystemen ausgegeben
hex
werden: Oktal-, Dezimal- oder Hexadezimalsystem
showbase
Zeigt bei Oktalzahlen „0“ und bei Hexadezimalzahlen „0x“ vor jeder
Zahl an, was standardmäßig nicht der Fall ist.
noshowbase Hebt showbase wieder auf.
uppercase
Alle Buchstaben werden groß geschrieben.
nouppercase Alle Buchstaben werden klein geschrieben.
bool-Variablen
Manipulator
Beschreibung
52
OOM/OOP
boolalpha
Noboolalpha
Die Ausgabe des bool’schen Werts erfolgt als Wort (true/false).
Die Ausgabe des bool’schen Werts erfolgt als Zahl (0/1).
Fließkommavariablen
Manipulator
showpoint
Beschreibung
Eine Fließkommazahl wird immer mit Dezimalpunkt und
Nachkommastelle ausgegeben, z.B. 25 wäre dann 25.000
noshowpoint Bei einer Fließkommazahl werden nur die relevanten Stellen
ausgegeben, z.B. 25 wäre dann 25
scientific Gibt die Fließkommazahl in wissenschaftlicher Schreibweise aus,
z.B. 25,43 erscheint so: 2.543000e+001
fixed
Hebt scientific auf.
Setprecision Zur Angabe der Genauigkeit der Zahlenausgabe (Anzahl der
(x)
Nachkommastellen)
Anmerkung: Es gelten ebenfalls die allgemeinen Manipulatoren, die bei den
Ganzzahlen aufgelistet sind (left, right, …). Allerdings geleten die Manipulatoren
aus dieser Tabelle nur für Fließkommazahlen, d.h. es muss im Fall einer Ausgabe
von Ganzzahlwerten immer ein Punkt hinter der Zahl folgen, damit diese als
Fließkommazahl behandelt wird. Sonst hat die Formatierung mit den Manipulatoren
aus dieser letzten Tabelle keine Auswirkung.
#include <iostream.h>
#include <iomanip.h>
#include <stdio.h>
int main()
{
cout << "Formatierte Ausgabe von Ganzzahlen:\n" << endl;
cout.flags(ios::showbase);
cout << "Dezimalsystem: "<< setw(11) << dec << 165
<< "\n"
<< "Oktalsystem: " << setw(13) << oct << 165 << "\n"
<< "Hexadezimalsystem: " << setw(7) << hex << 165
<< endl;
cout << "\n\n";
cout << "Formatierte Ausgabe von Fliesskommazahlen:\n"
<< endl;
cout.flags(ios::left | ios::showpoint);
cout << setw(26) << "Dezimalzahl mit Nullen: "
<< setprecision(5) << 0.0165 << endl;
cout.unsetf(ios::showpoint);
cout << setw(26) << "Dezimalzahl ohne Nullen: "
<< setprecision(5) << 0.0165 << endl;
OOM/OOP
53
cout.flags(ios::scientific | ios::left);
cout << setw(26) << "Wissenschaftlich: " << setw(10)
<< setprecision(3) << 0.0165 << endl;
cout << "\n\n";
cout << "Zahlen in einer Tabelle mit Vorzeichen +/ausgeben:\n" << endl;
cout.flags(ios::showpos);
cout << setw(8) << 12 << setw(8) << 179 << '\n'
<< setw(8) << -25 << setw(8) << -3 << '\n'
<< setw(8) << -368 << setw(8) << 35 << endl;
cout << "\n\n";
getchar();
return(0);
}
Formatierte Ausgabe von Ganzzahlen:
Dezimalsystem:
Oktalsystem:
Hexadezimalsystem:
165
0245
0xa5
Formatierte Ausgabe von Fliesskommazahlen:
Dezimalzahl mit Nullen:
Dezimalzahl ohne Nullen:
Wissenschaftlich:
0.016500
0.0165
1.650e-002
#include <iostream.h>
#include <iomanip.h>
#include <stdio.h>
int main()
{
// 2 Varianten, um das Ausgabeformat zu definieren
// 1. Möglichkeit:
cout.width(10);
cout.flags(ios::left);
cout << "Laenge";
cout.width(6);
54
OOM/OOP
cout.precision(2);
cout.flags(ios::fixed | ios::showpoint | ios::right);
cout << 74.5 << " m" << endl;
// 2. Möglichkeit, um die selbe Ausgabe zu erreichen:
cout << setw(10) << setiosflags(ios::left) << "Laenge"
<< resetiosflags(ios::left) << setw(6)
<< setprecision(2) << setiosflags(ios::fixed |
ios::showpoint | ios::right) << 74.5 << " m"
<< endl;
getchar();
return(0);
}
Zahlen in einer Tabelle mit Vorzeichen +/- ausgeben:
+12
-25
-368
Laenge
Laenge
3.9
+179
-3
+35
74.50 m
74.50 m
Operatoren
Operatoren verbinden Werte zu neuen Werten. Hierzu existiert in C++ eine Vielfalt
unterschiedlicher Operatoren.
Ausdruck:
[Operand] Operator [Operand]
Wirkung: Operatoren dienen der Verknüpfung von Operanden
(Konstanten, Variablen und Funktionsaufrufen) zu
einem Ausdruck.
•
In C++ gibt es unitäre, binäre und terziäre Operatoren, welche ein, zwei oder drei
Operanden besitzen.
•
Ein Ausdruck kann sich aus Teilausdrücken zusammensetzen.
•
Kommen in einem Ausdruck mehrere Operatoren vor, so regelt eine für jeden
Operator festgelegte Bindungsstärke die Reihenfolge der Auswertung.
OOM/OOP
•
55
Höchste Bindungsstärke kann durch Klammerung erreicht werden.
Siehe den Ausdruck a + c * d oder (a + c) * d.
•
Jeder Ausdruck wird bewertet und liefert einen Wert, dessen Typ in
Abhängigkeit der Operanden und des Operators gebildet wird.
Beispiel:
a( −b / 2) − 3c
− 3( x + y + z 2 )
ergibt
(a*(-b/2.0)-3*c)/(-3*(x+y+z*z))
3.9.1 Liste der Operatoren
Die Priorität der Operationen nimmt von oben nach unten abschnittsweise ab.
Die Spalten der Tabelle enthalten:
1. Operation
2. Operator
3. Ergebnis für die Werte: short x = 2, y = 3, z = 1
4. Assoziativität, Bewertungsreihenfolge der Operatoren
von links nach rechts oder rechts nach links.
1
Klammern
Funktionsaufruf
Feldindex
StrukturElement
StrukturElement
Minus
Inkrement
Dekrement
Adresse
Größe Variable
Größe Datentyp
Komplement
Negation
Dereferenz
Cast
Multiplikation
Division
Modulus
Addition
Subtraktion
2
(x)
function(x)
array[x]
struct->elem
struct.elem
-x
++x
x++
--x
x-&x
sizeof x
sizeof (short)
~x
!x
*pointer
(type)
x*y
x/y
x%y
x+y
x-y
3
2
-2
3
2
1
2
Adresse x
2
2
-3
0
Wert
Typwandlung
6
0
2
5
-1
4
links
links
links
links
links
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
links
links
links
links
links
56
OOM/OOP
Bit-Shift links
Bit-Shift rechts
Kleiner
Kleinergleich
Größer
Größergleich
Gleich
Nicht gleich
Bit AND
Bit XOR
Bit OR
Logisches AND
Logisches OR
Konditional
Zuweisung
Komma
x << y
x >> y
x<y
x <= y
x>y
x >= y
x == y
x != y
x&y
x^y
x|y
x && y
x || y
z?x:y
x=y
x *= y
x /= y
y %= y
x += y
x -= y
x <<= y
x >>= y
x &= y
x |= y
x ^= y
x,y
16
0
1
1
0
0
0
1
2
1
3
1
1
2
3
6
0
2
5
-1
16
0
2
1
1
3
links
links
links
links
links
links
links
links
links
links
links
links
links
Rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
rechts
Links
3.9.2 Arithmetische Operatoren
•
Operationen auf float-Typen werden in C++ mit double-Genauigkeit durchgeführt.
•
Die Operatoren +, -, *, / sind auf int- und float (double)-Operanden anwendbar.
Bei unterschiedlichen Typen wird die Operation in double durchgeführt.
3.0 * 2
•
ergibt
6.0
Es ist zu beachten, dass bei der Division / mit int-Operanden zur Null hin
abgebrochen wird.
3 / 4
-11 / 3
ergibt
ergibt
0
-3
•
Die Operatoren ++, -- und % sind nur auf int-Operanden anwendbar.
•
Die Inkrement- (--) und Dekrement- (++) Operatoren erhöhen bzw. erniedrigen
den Wert ihres Operanden. Die Anwendung vor oder nach dem Operanden
beeinflusst den Zeitpunkt der Bewertung des Operanden.
OOM/OOP
int
i =
j =
i =
57
i,j;
2;
++i; /* erhöhe i um 1 und weise j 3 zu */
j--; /* weise i den Wert 3 zu und
erniedrige j um 1 */
3.9.3 Logische Operatoren
•
Logische Operatoren sind <, <=, >, >=, &&, ||, !, ==.
•
Man beachte die Verwechslung zwischen dem Zuweisungsoperator = und dem
Identitätsoperator ==.
•
In C existiert kein logischer (wahr, falsch) Datentyp. Obwohl mit C++ ein boolscher
Datentyp eingeführt wurden gilt noch die folgende Definition:
•
wahr:
Ausdruck ungleich Null bewertet
falsch:
Ausdruck identisch Null bewertet
Die Bewertung einer Relation ergibt immer 0 oder 1.
int i = 3;
(i == 3) ==
i;
i > 3;
1;
/* Wahr
*/
/* Wahr
*/
/* Falsch */
•
Die Operatoren <, <=, >, >= können auf int- und float-Operanden angewendet
werden. Alle weiteren Operatoren sind nur auf int-Operanden anzuwenden, d.h.
zwei Gleitkommawerte z.B. können nicht auf Identität mit == geprüft werden.
•
Die Auswertung von logischen Ausdrücken wird abgebrochen, wenn das
Ergebnis feststeht.
3.9.4 Zuweisungsoperatoren
•
Der linke Operand muss einen L-Wert darstellen.
Beispiel:
i = i + 27 entspricht i += 27
•
Mit dem Operator = (und der Operatorenklasse +=, *= usw.) wird dem L-Wert der
Wert des rechten Operanden zugewiesen.
3.9.5 Adress-Operator &
•
Der Operator & liefert die Adresse des Operanden im Hauptspeicher.
58
•
OOM/OOP
Der Operand muss ein L-Wert sein.
int i;
/* int-Variable i */
cout << &i;
/* Es wird die Adresse der
Variablen i ausgegeben */
3.9.6 Dereferenzoperator *
•
Bewertet einen Adressausdruck zu einem L-Wert vom Grundtyp des Ausdruckes.
•
Im einfachsten Fall wird zu einer Pointervariablen der L-Wert verlangt.
int k = 1;
/* int-Variable k */
int* kptr;
/* Zeiger auf int */
kptr = &k;
/* Pointer bekommt Adresse von k */
*kptr = 2;
/* L-Wert von k wird geändert
*/
cout << "k= " << *kptr;
/* Ausgabe von "k=2" */
main()
{
int i = 0, j = 1, wahr, falsch;
wahr = i == 0;
falsch = j == 0;
falsch = j != j;
wahr = i || j;
falsch = (i == 1) && (j > 10);
}
3.9.7 Der scope-resolution Operator „::“
Neben der Verwendung von „::“ im Zusammenhang mit Klassen erlaubt der scoperesolution Operator einen Zugriff auf versteckte globale Objekte.
class cKlasse
{
int printf(char * pszName);
void ausgabe();
};
OOM/OOP
59
void cKlasse::ausgabe()
{
::printf("Hallo Welt"); // function printf aus stdio.h
printf("Hallo Welt");
// function printf aus cKlasse;
}
3.10 Klassen I
3.10.1 Die Klasse
Nachteile von Strukturen:
• Änderungen sind zeitintensiv
•
es können keine wirklichen Datentypen definiert werden
→ Eine Klasse ist die Beschreibung eines benutzerdefinierten abstrakten Datentyps.
Die Klasse beinhaltet:
•
die Typdefinition
•
die Deklaration der Datenelemente (Attribute)
•
die Deklaration der zugelassenen Operationen (Methoden)
Syntax:
class Name
{
private:
…
public:
…
protected:
…
};
•
Mit der Klassendefinition wird automatisch der Typname festgelegt. Die
Anweisung typedef entfällt.
•
Den Schutzmechanismus führt die Klasse durch Schutzbereiche ein (private,
public, protected).
class ratio
{
60
OOM/OOP
private:
int z;
// Zähler
int n;
// Nenner
public:
void print( );
ratio addiere(ratio ∗r2);
};
//Semikolon am Ende wichtig
Begriffe:
Die Elemente einer Klasse können Daten- oder Funktionsdeklarationen sein.
Datenelement
Funktionen
→
→
Eigenschaft oder Membervariable
Methoden oder Memberfunktionen
Alle Eigenschaften zusammen bilden den Zustand eines Objektes.
Variablen, die mit Hilfe der Klassendefinition angelegt (instanziiert) werden, heißen
Objekte (Instanzen einer Klasse).
Schutzbereiche:
a) private:
Als private werden die Elemente einer Klasse definiert, auf die nur innerhalb der
eigenen Klasse zugegriffen werden darf. Diese Elemente können von außen nicht
manipuliert werden.
b) public:
Dem gegenüber werden als public all die Elemente definiert, die keinerlei
Zugriffsbeschränkung unterliegen.
c) protected:
Auf Elemente, die als protected definiert worden sind, kann innerhalb der eigenen
Klasse und allen abgeleiteten Klassen zugegriffen werden.
→ public, private und protected können dabei mehrfach auftreten.
Zugriff:
Analog zu der Arbeit mit Strukturen kann nun Speicherplatz für eine bestimmte
Anzahl von Objekten reserviert werden.
ratio A,B,*C;
C = &A;
Ganz allgemein kann man mit dem Punkt- oder Pfeiloperator auf Elemente eines
Objektes zugreifen. Da nun auch Methoden Elemente der Klasse sind, mit der das
OOM/OOP
61
Objekt angelegt wurde, kann man auch Methoden mit einem Punkt für ein
bestimmtes Objekt aufrufen.
A.print();
C->print();
Implementierung der Methoden:
Dem Namen der Methode muss man mit Hilfe des neuen Bereichsoperators den
Namen der zugehörigen Klasse voranstellen. Die Zuordnung der Methode zu einer
Klasse benutzt der Compiler zur Überprüfung.
Syntax:
Typ <Basisklasse>::<Methodenname>([Parameterliste])
void ratio::print( )
{
cout << z << "/" << n;
}
ratio ratio::addiere(ratio ∗op2)
{
ratio erg;
erg.z = z ∗ op2->n + n ∗ op2->z;
erg.n = n ∗ op2->n;
return erg;
}
3.10.2 Konstruktoren und Destruktoren
Konstruktor
→ Initialisierung
In der Klasse wird eine spezielle Methode deklariert. Sie heißt genauso wie die
Klasse und hat keinen Rückgabewert. Diese Methode wird Konstruktor genannt.
Sie wird vom Compiler automatisch aufgerufen, wenn eine Variable angelegt wird.
Der Konstruktor kann alles erledigen was ein Objekt am Anfang seiner Lebensdauer
benötigt, nicht nur Anfangswerte setzen.
Beispiele hierfür sind:
62
OOM/OOP
− dynamisch Speicherplatz anlegen
− ein Fenster am Bildschirm entstehen lassen
z.B.: → Implementierung des Konstruktors
#include "ratio.h"
ratio::ratio(int zae, int ne)
{
z = zae;
n = ne;
}
// alle weiteren Methoden...
→ Anlegen von initialisierten Objekten
#include "ratio.h"
ratio A (1,2);
Destruktor
C++ bietet auch die Möglichkeit für jede Klasse eine Methode zu schreiben, die am
Ende der Lebensdauer eines Objektes automatisch aufgerufen wird: den Destruktor.
Es handelt sich um eine typ- und parameterlose Funktion, deren Name aus
Klassenname mit einer davorgesetzten ∼ (Tilde) gebildet wird.
z.B.: Klasse mit Destruktor
#include<stdio.h>
class ratio
{
…
public:
ratio(int z = 0,int n = 1);
∼ratio();
{
delete data;
}
…
}
// auch initialisierte
// Parameter möglich
// Destruktor
// data wurde im
// Konstruktor erzeugt
OOM/OOP
63
64
OOM/OOP
Beispiele für die Verwendung eines Destruktors sind:
− dynamisch angelegten Speicherplatz zurückgeben
− bei einem Fenstersystem für den PC könnte man den Destruktor für ein Objekt
“Fenster“ benutzen, um das Fenster wieder vom Bildschirm zu entfernen
3.11 Typkonvertierungen
3.11.1 Implizite Typkonvertierungen
•
In Ausdrücken und bei Zuweisungen erfolgen ggf. Typumwandlungen.
•
Vor der Berechnung eines arithmetischen Ausdruckes werden die folgenden
Typkonvertierungen ausgeführt:
char, short
float
•
Ö int
Ö double
Sind nach der Konvertierung die Operanden unterschiedlich, werden weitere
Konvertierungen in folgender Reihenfolge vorgenommen:
1. Ist ein Operand vom Typ double, so werden die anderen
Operanden und der Ausdruck zum Typ double.
2. Ist ein Operand vom Typ long, so werden die anderen
Operanden und der Ausdruck zum Typ long.
3. Ist ein Operand vom Typ unsigned, so werden die anderen
Operanden und der Ausdruck unsigned.
•
Bei einer Zuweisung bestimmt die linke Seite (L-Wert) den Typ, auf den ggf.
konvertiert wird.
3.11.2 Explizite Typkonvertierung
Der cast-Operator
•
dient der Umwandlung von Typen,
(type) Ausdruck
•
ist erforderlich, wenn ein bestimmter Datentyp gefordert wird und nicht vorhanden
ist,
•
liefert keinen L-Wert.
OOM/OOP
65
3.12 Anweisungen
Anweisungen beschreiben Aktionen auf Variablen und kontrollieren den Ablauf eines
Programms. Hierdurch wird der Algorithmus in einem Programm gebildet. Die zur
Kontrolle eingesetzten Anweisungen werden auch als Kontrollkonstrukte bezeichnet.
In C++ lassen sich Anweisungen in folgende Gruppen unterteilen:
Anweisungen:
Ausdrucksanweisung
Wertzuweisung
leere Anweisung
Blockanweisung
bedingte Anweisungen
if
else-if
switch
Wiederholungsanweisungen
while
do-while
for
Sprunganweisungen
goto
continue
break
return
Wirkung: Steuerung des Programmablaufes und der Datenmanipulation
3.12.1 Ausdrucksanweisung
Eine Ausdrucksanweisung wird durch einen Ausdruck gefolgt von einem Semikolon
gebildet.
Ausdrucksanweisung:
[Ausdruck] ;
Wirkung: Der Ausdruck wird ausgeführt und bewertet. Ist kein
Ausdruck vorhanden, so ist dies eine leere
Anweisung.
Mögliche Anwendungen von Ausdrucksanweisungen sind die leere Anweisung,
Funktionsaufrufe ohne Wertzuweisung oder Wertzuweisungen auf L-Werte.
66
OOM/OOP
3.12.2 Leere Anweisung
Das Semikolon bedeutet das Ende einer Anweisung. Eine Anweisung ohne Ausdruck
dient der Leserlichkeit eines Programms. Eine Anwendung erfolgt beispielsweise bei
Sprungzielen.
Beispiel:
{
[Anweisungen, ...]
goto Schluss;
[Anweisungen; ...]
Schluss:
;
}
/* leere Anweisung */
Das Sprungziel Schluss dient der Verzweigung auf eine leere Anweisung zum Ende
dieses Beispiels.
3.12.3 Funktionsaufrufe
Eine Anweisung kann nur aus einem Funktionsaufruf bestehen. Nach der Bewertung
der Funktion kann deren Rückgabewert unbeachtet bleiben. In diesem Fall wird eine
Funktion im prozeduralen Sinne wie ein Unterprogramm benutzt (FORTRAN:
subroutine, PASCAL: procedure).
double x;
x = sqrt(81);
/* Quadratwurzel aus 81 */
3.12.4 Wertzuweisung
Eine häufige Anwendung einer Ausdrucksanweisung ist die Wertzuweisung.
Wertzuweisung:
L-Wert = Ausdruck ;
Wirkung: Der Ausdruck wird bewertet und dem L-Wert
zugewiesen.
•
Die Zuweisung eines Wertes aus einem bewerteten Ausdruck muss auf ein
modifizierbares Datenobjekt, dem sogenannten L-Wert erfolgen.
OOM/OOP
67
•
Ist der L-Wert auf der linken Seite von einem anderen Typ als der Werttyp des
Ausdrucks, so erfolgt eine implizite Typkonvertierung bei der Zuweisung auf den
L-Werttyp.
•
Typkonvertierungen können möglicherweise Informationsverlust verursachen.
z.B.:
double-Werttyp auf float-L-Werttyp durch Rundung oder
long-Werttyp auf short-L-Werttyp durch Unterdrücken
signifikanter Bits
main()
{
/*
Deklaration der Ganzzahl-Variablen i und j */
int i,j;
/*
Bewertung des Ausdruckes 2 als Wert 2 und
Zuweisung an Variable i */
i = 2;
/*
Bewertung des Ausdruckes 2 * i + 1 als Wert 5
und Zuweisung an die Variable j */
j = 2 * i + 1;
/*
Bewertung des Ausdruckes 3.14 * j als
Wert 15.7, Typwandlung auf L-Werttyp int und
Zuweisung des Wertes 15 an Variable i */
i = 3.14 * j;
/*
Ordnungsgemäßes Beenden des Programms * /
exit(0);
}
3.12.5 Blockanweisung
Durch eine Blockanweisung erfolgt eine Zusammenfassung mehrere Anweisungen.
Eine Blockanweisung kann immer anstelle einer einfachen Anweisung angegeben
werden. Sie wird syntaktisch als nur eine Anweisung betrachtet.
Blockanweisung:
{
[Deklarationen]
Anweisung, Anweisung, ...
}
Wirkung: Die Anweisungen werden in sequentieller Reihenfolge ausgeführt. Deklarationen zu Beginn des
Blockes sind optional.
•
Der Deklarationsteil am Anfang eines Blockes kann entfallen.
68
OOM/OOP
•
Eine Blockanweisung wird nicht mit einem Semikolon beendet.
•
Werden Variablen in einem Deklarationsteil vereinbart, so besitzen sie eine lokale
Gültigkeit nur innerhalb dieses Blockes.
•
Bei identischen Bezeichnern werden durch frühere Deklarationen entstandene
Vereinbarungen außer Kraft gesetzt und erst am Ende des Blockes
wiederhergestellt.
•
Initialisierungen von auto und register Variablen finden sequentiell bei jedem
Erreichen eines Blockes statt. Durch einen goto Sprung ist es möglich, beliebig in
einen Block einzutreten und somit die Initialisierung zu vermeiden. Dies entspricht
allerdings einem schlechten Programmierstil.
•
Variablen der Speicherklasse static werden nur einmal zu Programmstart
initialisiert.
•
Für extern deklarierte Variablen wird kein Speicherplatz im Block reserviert und
somit kann keine Initialisierung erfolgen.
3.12.6 Bedingte Anweisungen
Diese Kontrollkonstrukte dienen der Auswahl einer Anweisung. Sind mehrere
Anweisungen in einer Abhängigkeit auszuführen, so sind diese in einer
Blockanweisung zusammenzufassen.
3.12.6.1
if-Anweisung
if-Anweisung:
if ( Ausdruck ) Anweisung
Wirkung: Der Ausdruck wird bewertet und für logisch wahr
(Wert ungleich 0) die erste Anweisung nach dem
Ausdruck ausgeführt.
OOM/OOP
69
#include “iostream.h”
main()
{
int i = 1;
if (i)
cout << „erstes if\n“;
if (!i)
cout << „zweites if wird nie geschrieben\n“;
if (i == 1)
{
cout << „Mehrere Anweisungen “;
cout << „durch Blockanweisung “;
cout << „zusammengefasst.\n“;
}
exit(0);
}
3.12.6.2
else-if-Anweisung
else-if-Anweisung:
if ( Ausdruck ) Anweisung else Anweisung
Wirkung: Der Ausdruck wird bewertet und für logisch wahr
(Wert ungleich 0) die erste Anweisung nach dem
Ausdruck ausgeführt. Wird der Ausdruck mit logisch
falsch (Wert identisch 0) bewertet, so wird die
Anweisung nach dem else ausgeführt.
#include “iostream.h”
main()
{
int i = 0, j = 1;
if (i == 0 && j == 1)
{
cout << "i ist identisch Null und ";
cout << "j ist identisch Eins\n";
}
if (j == 1)
cout << "j ist identisch Eins\n";
70
OOM/OOP
else
cout << "j ist nicht identisch Eins\n";
}
3.12.6.3
switch-Anweisung
Mittels der switch-Anweisung wird eine von mehreren Anweisungen ausgeführt.
switch-Anweisung:
switch (Ausdruck)
{
case KonstanterAusdruck1: Anweisungen break;
case KonstanterAusdruck2: Anweisungen break;
[default
: Anweisungen]
}
Wirkung: Der Ausdruck nach switch wird bewertet. Kommt
der Wert unter den Konstanten nach case vor, so
wird der Programmablauf bei den zugehörenden
Anweisungen fortsetzt. Die break-Anweisung dient
dem Verlassen der switch-Anweisung.
Die optionale default-Anweisung wird ausgeführt,
falls keine Übereinstimmung des switch-Ausdruckes
mit case Ausdrücken vorliegt.
•
Die Bewertung des switch-Ausdruckes muss als Resultat einen Wert vom Typ int
liefern (char und short werden in int umgewandelt, long-Werte je nach
Implementierung des C-Compilers).
•
Die case-Ausdrücke müssen konstante Ausdrücke des Typs int sein (charKonstanten werden in int-Konstanten umgewandelt, long- Werte s.o.).
•
Die einem case folgenden Ausdrücke werden sequentiell bis zum Erreichen einer
break-Anweisung ausgeführt.
Beachte: Ist keine break-Anweisung vorhanden, werden auch eventuell folgende
case-Anweisungen ausgeführt (im Gegensatz zur PASCAL select/case-Anweisung).
Ein case-Ausdruck kann bei Identität zum switch-Ausdruck als Einsprungmarke in
eine Folge von Ausdrücken aufgefasst werden.
OOM/OOP
71
Beispiel:
#include “iostream.h”
main()
{
int k = 2;
switch (k)
{
case 0:
cout << "k ist identisch 0";
break;
case 1:
cout << "k ist identisch 1";
break;
case 2:
case 3:
default:
cout << "k ist nicht identisch 0,1";
}
}
3.12.7 Wiederholungsanweisungen
Wiederholungsanweisungen dienen dem mehrfachen Ausführen einer Anweisung
oder mehrerer Anweisungen in einem Anweisungsblock.
3.12.7.1
while-Anweisung
Die while-Anweisung stellt ein kopfgesteuertes Schleifenkonstrukt dar. Eine
Anweisung wird nach dem Auswerten eines Ausdruckes ggf. wiederholt ausgeführt.
while-Anweisung:
while
(Ausdruck)
Anweisung
Wirkung: Der Ausdruck (s.o. Abbruchbedingung) nach while wird bewertet. Ist dessen Wert ungleich Null (logisch wahr), so wird
die Anweisung ausgeführt. Ein mit Null bewerteter Ausdruck
(logisch falsch) bewirkt den Abbruch der Ausführung.
•
Als kopfgesteuerte Schleife wird die while-Schleife möglicherweise nie
durchlaufen.
•
Mehrere Anweisungen einer while-Schleife sind in einer Blockanweisung
zusammenzufassen.
72
OOM/OOP
•
Eine Blockanweisung kann durch Sprunganweisungen vorzeitig verlassen
werden.
•
Die Abbruchbedingung ist hinsichtlich der Möglichkeit einer Endlosschleife zu
überprüfen.
Beispiel:
#include <stdio.h>
#include <iostream.h>
main()
{
int i = 0, k = 10;
/* 10-fache Ausführung der cout-Anweisung */
while (i != 10)
{
i = i + 1;
cout << "i hat den Wert \n" << i;
}
/* Endlosschleife */
while (i >= 10)
i = i + 1;
/* Schleife mit Dekrementierung der Variablen k */
while (k--)
cout << "k hat den Wert \n" << k;
exit(0);
}
3.12.7.2
do-while-Anweisung
Die do-while-Anweisung stellt ein fußgesteuertes Schleifenkonstrukt dar. Die
Abbruchbedingung der Wiederholung wird am Ende der Schleife ausgewertet.
do-while-Anweisung:
do Anweisung while (Ausdruck);
Wirkung: Die Abhängige Anweisung wird so lange wiederholt bis der
Wert des Ausdruckes ungleich Null (logisch falsch) ist. Ein mit
Null bewerteter Ausdruck (logisch falsch) bewirkt den Ab-
OOM/OOP
73
bruch der Anweisungsausführung.
•
Im Gegensatz zur while-Anweisung wird die abhängige Anweisung der do-whileAnweisung mindestens einmal ausgeführt.
•
Mehrere Anweisungen einer do-while-Schleife sind in einer Blockanweisung
zusammenzufassen.
•
Eine Blockanweisung kann durch Sprunganweisungen vorzeitig verlassen
werden.
•
Die Abbruchbedingung ist bezüglich der Möglichkeit einer Endlosschleife
abzuklären.
Beispiel:
#include <stdio.h>
#include <iostream.h>
main()
{
int i = 0, k = 10;
do /* 10-fache Ausführung der print-Anweisung */
{
i = i + 1;
cout << "i hat den Wert \n" << i;
}
while (i != 10);
do /* Endlosschleife */
i = i + 1;
while (i !=10);
do /* Schleife mit Dekrement der Variablen k */
cout << "k hat den Wert \n" << k;
while (k--);
}
74
OOM/OOP
3.12.7.3
for-Anweisung
Die for-Schleife ist geeignet, um beginnend von einem Startwert aus mit einer
bestimmten Schrittweite bis zu einem Endwert zu zählen.
for-Anweisung:
for ([Ausdruck1]; [Ausdruck2]; [Ausdruck3]) Anweisung
Wirkung: Ausdruck1 wird nur einmal ausgeführt und bietet die
Möglichkeit der Initialisierung von Variablen.
Ausdruck2 beschreibt eine Abbruchbedingung
Entsprechend der while-Schleife zu Beginn einer
Wiederholung.
Ausdruck3 wird nach der Ausführung der Anweisung
Ausgewertet und dient der Inkrementierung einer Variablen
Entsprechend einer Schrittweite.
•
Mehrere Anweisungen
zusammenzufassen.
•
Eine Blockanweisung kann durch Sprunganweisungen vorzeitig verlassen
werden.
•
Ist der Ausdruck2 nicht vorhanden, so wird er als wahr bewertet angenommen
und es erfolgt kein Abbruch der for-Schleife. Die Schleife muss dann explizit mit
einer Sprunganweisung (break, goto, return) verlassen werden.
•
Ausdruck1, Ausdruck2 und Ausdruck3 sind optional und müssen nicht vorhanden
sein.
einer
for-Schleife
sind
in
einer Blockanweisung
Beispiel:
#include <stdio.h>
#include <iostream.h>
main()
{
int i = 0, k = 0;
for (i = 0; i < 10; i++)
cout << "Variable i = " << i << " \n");
for ( ; ; )
{
if (k >= 10) break;
cout << "Variable k = " << k << " \n";
k++;
}
OOM/OOP
75
}
3.12.8 Sprunganweisungen
Sprunganweisungen dienen dem Verzweigen des Programmablaufes zu einer
explizit angebbaren Anweisung.
3.12.8.1
goto-Anweisung
Die allgemeine Sprunganweisung wird durch die goto-Anweisung dargestellt.
goto-Anweisung:
goto
Bezeichner;
Marke:
Bezeichner: Anweisung
Wirkung: Es wird die mit der Marke Bezeichner gekennzeichnete
Anweisung ausgeführt.
•
goto-Anweisung und -Marke sind auf
Anweisungsblockes einer Funktion beschränkt.
•
Bei einem Sprung in einen Anweisungsblock werden eventuell vorhandene
Initialisierungen von lokalen Variablen umgangen.
•
goto-Anweisungen sind zu vermeiden. Häufige Verzweigungen erschweren es
dem Programmierer die Logik eines Programms nachzuvollziehen.
•
goto-Anweisungen sind sinnvoll, um aus Verschachtelungen von Schleifen oder
Bedingungsanweisungen herauszuspringen.
den
Geltungsbereich
Beispiel:
#include <stdio.h>
#include <iostream.h>
main()
{
int k = 0;
for( ; ; )
{
k++;
/* Sprunganweisung zur Marke Weiter */
if (k > 10)
des
76
OOM/OOP
goto Weiter;
}
/* Sprungziel mit Marke Weiter */
Weiter: cout << "Ende der Schleife.";
}
3.12.8.2
break-Anweisung
Führt eine unbedingte Verzweigung ans Ende einer Schleife oder switch-Anweisung
aus.
break-Anweisung:
break;
Wirkung: Abbruch der zugehörigen for- ,while- ,do- oder switch-Anweisung, in deren Abhängigkeit die break-Anweisung vorkommt. Die Programmausführung wird mit der Anweisung
fortgesetzt, welcher der abgebrochenen Anweisung direkt
folgt.
•
In switch-Anweisungen ist die break-Anweisung zum Beenden von case
folgenden Anweisung notwendig. Ansonsten ergibt sich ein nicht beabsichtigtes
Ausführen mehrerer nachfolgender case bezogener Anweisungen.
•
In Schleifen dient die break-Anweisung einem besseren Programmierstil als die
goto-Anweisung zum Verlassen von Schleifen.
Beispiel:
#include <stdio.h>
#include <iostream.h>
main()
{
int i = 0;
/* Endlosschleife mit for( ; ; ) */
for ( ; ; )
{
i = i + 1;
switch (i)
{
case 0:
printf("i = 0\n");
break;
case 1:
printf("i = 1\n");
OOM/OOP
77
break;
default: printf("Beende die Schleife.\n");
}
/* Beenden der Schleife */
if (i != 0 && i != 1) break;
}
/* Nächste Anweisung nach Schleifenende */
printf("Die Schleife wurde mit break beendet.\n");
}
3.12.8.3
continue-Anweisung
Muss sich in Abhängigkeit zu einer do-, for- oder while-Schleife befinden und
bewirkt einen unbedingten Abbruch des aktuellen Schleifendurchlaufes.
continue-Anweisung:
continue;
Wirkung: In einer Schleife wird die Ausführung eines Programms bei
dem Ausdruck fortgesetzt, welcher über die Abbruchbedingung der Schleife entscheidet.
•
Der aktuelle Iterationsschritt der Schleife wird abgebrochen, nicht die Schleife.
•
Entspricht einem Sprung an das Blockende einer Schleifenanweisung.
Beispiel:
#include <stdio.h>
main()
{
int i = 0;
/* Beispiel mit goto-Anweisung */
while (i < 10)
{
i++;
if (i > 5)
goto next;
i++;
/* Hier wird die leere Anweisung benötigt */
next: ;
}
/* Beispiel mit continue */
78
OOM/OOP
while (i < 10)
{
i++;
if (i > 5)
continue;
i++;
}
}
3.12.8.4
return-Anweisung
Der Rücksprung aus einer Funktion wird durch die return-Anweisung ausgeführt.
return-Anweisung:
return [Ausdruck];
Wirkung: Bewirkt einen Rücksprung aus der aktuellen Funktion zur
aufrufenden Funktion. Der optionale Ausdruck wird bewertet
und von der Funktion als Rückgabewert eingesetzt.
•
Ist der Ausdruck nicht vorhanden, so ist der Rückgabewert der aufgerufenen
Funktion undefiniert und darf in der rufenden Funktion nicht benutzt werden. Die
gerufene Funktion muss syntaktisch korrekt den Typ void besitzen.
•
Ist ein Ausdruck vorhanden, so wird er ggf. nach seiner Bewertung auf den Typ
der Funktion umgewandelt (entsprechend einer Wertzuweisung).
•
Eine return-Anweisung ohne Ausdruck liegt auch vor, wenn das Ende des
Anweisungsblocks der Funktion erreicht wird.
•
In einer Funktion können mehrere return-Anweisungen vorkommen.
Beispiel:
#include <stdio.h>
int funktion1(int n)
{
if (n < 5)
return n;
/* Fehler tritt auf, wenn die Bedingung (n < 5) nicht
zutrifft. In diesem Fall wird ein implizites return
mit
undefiniertem Rückgabewert am Ende des
Anweisungsblockes von funktion1 erzeugt. */
OOM/OOP
79
}
void funktion2(void)
{
int k = 0;
while (k++ < 10)
printf("\nFunktion1= %d",funktion1(k));
}
main()
{
funktion2();
exit(0);
}
3.13 Funktionen
•
Funktionen werden eingesetzt:
Wenn eine Folge von Anweisungen in einem Programm mehrfach
auftritt.
-
⇒
Aufgaben eines Programms werden in Teilaufgaben zerlegt und in
Funktionen zusammengefasst.
Zur Gliederung großer Programme aus Gründen der Übersichtlichkeit
(modularer Aufbau).
-
⇒
Funktionen sind die modularen Bausteine eines C-Programms.
•
Funktionen sind eigenständige Programmeinheiten, welche von der mainFunktion oder von anderen Funktionen aufgerufen werden.
•
Funktionen sind das einzige Unterprogrammkonstrukt in C.
•
Funktionsdefinitionen können nicht geschachtelt werden.
•
Rekursive Funktionen sind möglich.
•
Datenaustausch zwischen aufrufender und aufgerufener Funktion erfolgt über
Parameterlisten oder globale Variablen.
•
Es existiert als Parameterübergabe-Mechanismus nur die Wertübergabe
(call by value).
main()
{
funktion();
80
OOM/OOP
void funktion()
{
/*
tue etwas
*/
return;
}
Alle Funktionen sind im Hauptspeicher hintereinander abgelegt.
Bild: Funktion main ruft Funktion funktion auf
OOM/OOP
81
3.13.1 Funktionsdefinition
Funktionsdefinition:
Typangabe Funktionsname (Deklaration_formeller_Parameter)
{
Deklaration_lokaler_Variablen;
Anweisungen
}
Wirkung: Beschreibt den semantischen Aufbau einer Funktion.
•
Es wird unterschieden zwischen den formellen Parametern (in der Definition) und
den aktuellen Parametern (beim Aufruf der Funktion).
•
Die Parameterdeklaration erlaubt einem C-Compiler eine Typüberprüfung.
•
Hat eine Funktion keine Parameter, so sind die Klammern () trotzdem nach dem
Funktionsnamen anzugeben.
•
Fehlt die Typangabe der Funktion, so ist der Funktionstyp implizit int.
•
Eine Funktion kann durch die return-Anweisung vorzeitig beendet werden.
•
Funktionen, die keinen Wert zurückgeben, sind vom Typ void.
Beispiel: Funktionsdefinition
/* Definition einer Minimum-Funktion
Funktion ist vom Typ int
formale Wertparameter i und j vom Typ int */
int minimum(int i, int j)
{
if (i > j)
return j;
return i;
}
/* Anwendung der Funktion */
void main(void)
{
printf("Minimum = %d\n",minimum(1,3));
}
82
OOM/OOP
3.13.2 Funktionsdeklaration
Eine Deklaration von Funktionen ist notwendig, wenn eine Definition der gerufenen
Funktion nach dem Aufruf oder in einer anderen Quelltext-Datei erfolgt.
Funktionsdeklaration:
[extern] Typangabe
Funktionsname (Deklaration_formeller_Parameter);
Wirkung:
Dem Compiler wird es ermöglicht, den korrekten
Funktionstyp in der Bewertung eines Funktionsaufrufes
einzusetzen. Zusätzlich kann der Compiler eine Überprüfung
zwischen aktuellen und formellen Parametern vornehmen.
•
Das Schüsselwort extern bezieht sich auf den Geltungsbereich von Objekten.
•
Die Angabe von extern kann bei Funktionen (im Gegensatz zur Speicherklasse
externer Variablen) entfallen.
•
Deklarationen können außerhalb von Funktionen (sind allen nachfolgenden
Funktionen bekannt) oder innerhalb von Funktionen zu Beginn eines
Anweisungsblockes (sind nur in diesem Block bekannt) stehen.
•
Die Deklaration von Funktionen erfolgt üblicherweise in eigenen Dateien, den
Header-Dateien (Dateiname.h), welche durch die Präprozessor-Anweisung
#include "Dateiname.h" zur Übersetzungszeit in den C-Quelltext eingebunden
werden.
•
In der Deklaration kann die Angabe von Bezeichnern in der Deklarationsliste der
formellen Parameter entfallen.
extern int funktion(int i, double r);
/* mit Bezeichnern */
extern int funktion(int, double);
/* ohne Bezeichner */
OOM/OOP
83
Beispiel: Definition und Deklaration von Funktionen
#include <stdio.h>
void main()
{
double r = 2.0, a;
extern double flaeche(double radius); /* Deklaratio
*/
a = flaeche(r);
/* Aufruf */
printf("Flaeche A = %lf\n",a);
}
double flaeche(double radius)
/* Definition */
{
return 2 * 3.1415*radius;
}
3.13.3 Funktionsaufrufe
Ein Funktionsaufruf besteht aus der Angabe des Funktionsbezeichners mit einer
Liste von Ausdrücken, den aktuellen Parametern (Argumenten) der Funktion.
Funktionsaufruf:
Funktionsname (Liste_aktueller_Parameter);
Wirkung:
Der Funktionsaufruf stellt einen Ausdruck dar, der in folgenden Schritten bewertet
wird:
1) Bewertung der Ausdrücke der aktuellen Parameter und Übergabe an die
Funktion.
2) Verzweigung der Programmausführung zu der gerufenen Funktion und
Auswertung der Anweisungen innerhalb der Funktion.
3) Beendet eine return-Anweisung mit einem Ausdruck die Funktion, dann wird der
Ausdruck bewertet und als Funktionswert zurückgegeben. Ansonsten ist der
Funktionswert undefiniert (void).
•
Der Aufruf einer Funktion stellt einen bewertbaren Ausdruck dar und kann somit
an allen Stellen bewertbarer Ausdrücke erscheinen.
•
Funktionen mit undefiniertem Funktionswert (void) dürfen nur als einzelne
Anweisung erfolgen.
•
Anzahl und Typ der aktuellen und formellen Parameter müssen übereinstimmen,
ansonsten kommt es zu fehlerhaften Ergebnissen.
84
OOM/OOP
•
Bei der Übergabe von Parametern und der Rückgabe von Funktionswerten findet
eine Typumwandlung wie in Ausdrücken statt. Die Typen char und short werden
nach int und float nach double umgewandelt und an die formellen Parameter
übergeben.
•
Alle Parameter werden als Werte übergeben (call by value).
- Jeder aktuelle Parameter (Ausdruck) wird ausgewertet.
- Der Wert wird dem formellen Parameter zugewiesen.
- Eine Variable als Argument bleibt von eventuellen Änderungen des
Inhalts des formellen Parameters in der Funktion unberührt.
•
Es existieren folgende Ausnahmen bei der Auswertung aktueller Parameter:
- Wird ein Feldbezeichner als Argument verwendet, so wird die Adresse
des ersten Feldelementes übergeben.
- Bei Funktionsbezeichnern wird die Adresse der Funktion übergeben.
•
Die Reihenfolge der Auswertung von Argumenten ist nicht bestimmt.
Beispiel: Funktionsaufrufe call by value
#include <stdio.h>
int funktion(int k, double w)
/*Definition */
{
printf("k=%d, w=%lf\n",k,w); /*Zeigt: k=1,w=1.0 */
k = 99;
/*Veraenderung von k */
w = 123.456;
/*Veraenderung von w */
printf("k=%d,w=%lf\n",k,w);/*Zeigt:k=99,w=123.456 */
return 0;
}
void main()
{
int i = 1;
/* Definition und Initialisierung*/
double r = 1.0; /* Definition und Initialisierung*/
printf("i=%d, r=%lf\n",i,r);
/* Zeigt: i=1,r=1.0 */
funktion(i,r);
/*Aufruf mit Wertparametern*/
printf("i=%d, r=%lf\n",i,r); /* Zeigt: i=1,r=1.0 */
}
/* Variablen i, r unveraendert */
Sollen in einer Funktion die Inhalte der Argumente verändert werden, erfolgt die
Parameterübergabe als "call by reference".
•
Als aktueller Parameter wird die Adresse eines L-Wertes übergeben.
OOM/OOP
85
•
Diese Adresse wird dem formellen Parameter in Form einer Pointer-Variablen
zugewiesen.
•
Durch Dereferenzierung mit dem Operator * kann der Inhalt des Datenobjektes
(bezeichnet durch den L-Wert) verändert werden.
•
Wird zum Beispiel bei der Eingabefunktion scanf benutzt, um Werte einzulesen.
#include <stdio.h>
void swap(double *x,double *y)
/* Definition */
{
double tmp = *x;
/* temoraere Variable
*/
*x = *y;
/* Vertauschung
*/
*y = tmp;
}
void main()
{
double a =2.0,
/* Definition und
*/
b =3.0;
/* Initialisierung
*/
swap(&a,&b);
/* Vertauschungsfunktion */
printf("%lf %lf\n",a,b); /* Ausgabe 3.0 und 2.0 */
}
3.13.4 Default-Parameterwerte
In C++ ist es bei Funktionsdefinitionen gestattet, den formalen Parametern DefaultWerte zu geben. Die formalen Parameter, die mit solchen Default-Werten
ausgestattet sind, dürfen beim Aufruf der Funktion weggelassen werden.
Default-Werte dürfen nur von links nach rechts angegeben werden.
Beispiele:
int funct1 (int a, double b, char c = ´c´);
int funct2 (int a, double b = 12.5, char c = ´c´);
legale Aufrufe:
funct1
funct1
funct2
funct2
(10, 3.14, ´a´);
(10, 3.14);
(10);
(10, ´c´);
Default für c wird überschrieben
Default für c schlägt zu
Werte für b und c aus Defaults nehmen
Fehler! Parameter in Mitte kann nicht
aus Default genommen werden
86
OOM/OOP
3.13.5 Overloading von Funktionen
Der gleiche Funktionsname kann für verschiedene Funktionen verwendet werden.
Die Funktionen müssen sich jedoch in ihren Parametertypen oder -anzahl
unterscheiden.
Die Anzahl der Default-Werte dient hierbei auch als Kriterium.
Beispiel: Betrag einer Zahl
int abs (int x) { /∗ Definition für int ∗/ };
double abs (double x) { /∗ Definition für double ∗/ };
3.13.6 Inline Deklarationen
In C++ können Funktionen als inline deklariert werden.
Das Schlüsselwort inline direkt vor der Funktionsdefinition bewirkt, dass überall dort,
wo die so gekennzeichnete Funktion aufgerufen wird, der komplette Code für diese
Funktion eingesetzt wird.
Beispiel:
inline double square (double x) // Def. von square( ) als inline
{
return (x ∗ x);
}
void main(void)
{
double a,b,y;
y = square (a - b);
}
// Einfügen des Codes für square
3.13.7 const-Deklaration
Das const-Schlüsselwort ist gewissermaßen das Gegenstück zum Präprozessor
#define in C++.Eine Variable, die mit const deklariert wurde, kann nach ihrer
Initialisierung nicht mehr verändert werden und kann nie auf der linken Seite einer
Zuweisung stehen.
OOM/OOP
87
Beispiel:
const double Pi = 3.1415;
void func(const int &anz)
{
anz = 5; //Fehler, da Wertzuweisung an Konstante nicht
erlaubt
}
3.14 Klassen II
3.14.1 Inline Methoden
Methoden können auch inline definiert werden. Dies kann direkt in der
Klassendefinition vorgenommen werden (s.o. Destruktor) oder mit dem
Schlüsselwort inline außerhalb der Klassendefinition.
Beispiel für obigen Konstruktor:
inline ratio::∼ratio() { delete data; // data wurde im Konstruktor erzeugt }
3.14.2 Überlagerung von Methoden
Der Begriff Überlagerung besagt, dass der gleiche Funktionsname mehrfach
verwendet werden darf. Damit der Compiler die verschiedenen Funktionen gleichen
Namens auseinanderhalten kann, verwendet er interne Namen. Dieser interne Name
setzt sich aus
− Klassennamen
− Liste der Parameter in codierter Form
zusammen.
88
OOM/OOP
z.B.: → Überlagern von Konstruktoren
class ratio
{
public:
ratio(int zae);
ratio(int zae,int ne);
};
→ Implementierung
ratio::ratio(int zae,int ne)
{
z = zae, n = ne;
}
ratio::ratio(int zae)
{
z = zae, n = 1;
}
// alle weiteren Methoden…
→ Anlegen von Objekten
ratio A (1,4), B (5);
Werden nun verschiedene Objekte mit unterschiedlichen Initialisierungslisten
angelegt, so wird der Compiler an Hand der Anzahl, der Reihenfolge und der Typen
der übergebenen Parameter die richtige Methode auswählen.
3.15 Vererbung
Die Vererbung ist eine Dienstleistung der Sprache, die die automatische
Wiederverwendung von Elementen der Basisklasse in abgeleiteten Klassen
ermöglicht.
3.15.1 Syntax (einfache Vererbung)
<abgeleitete Klasse> : [public, protected bzw. private] <Basisklasse>
{
...
};
OOM/OOP
89
3.15.2 Schutzkonzept
3 unterschiedliche Schutzstufen:
− private
− public
− protected
Die Schutzstufe protected erlaubt im Rahmen der Vererbung innerhalb der
abgeleiteten Klasse auf die Elemente der Basisklasse zuzugreifen.
class cBeispiel
{
private:
…
protected:
// auch für Methoden und Freunde abgeleiteter Klassen
…
public:
…
};
3.15.3 Offene (public) und geschlossene (private) Vererbung
Bei der Vererbung können die Schutzrechte der Basisklasse übernommen oder
eingeschränkt werden. In keinem Fall ist durch eine Vererbung die Erweiterung der
Schutzrechte möglich.
Zugriffsspezifizierung für Basisklassen
Ableitung
Member in Basisklasse
private
protected
public
private
private in abgeleiteten private in abgeleiteten
nicht zugreifbar Klassen
Klassen
protected
protected in
nicht zugreifbar abgeleiteten Klassen
protected in
abgeleiteten Klassen
public
protected in
nicht zugreifbar abgeleiteten Klassen
public in abgeleiteten
Klassen
3.15.4 Beispiel Basisklasse
90
OOM/OOP
class cAdresse
{
protected:
char name[50];
char vorname[50];
char straße[30];
char ort[40];
char telefon[20];
public:
int adr_eingeben;
int adr_schreiben;
int adr_listen;
};
3.15.5 Beispiel spezialisiertere Klasse
class cKunde : public cAdresse
{
protected:
int kundennummer;
int umsatz[12];
public:
int lies kundennummer( );
int lies_umsatz(int monat);
};
3.16 Einfache und mehrfache Vererbung
Einfache Vererbung
mehrfache Vererbung
3.16.1 Syntax
B
B
Wurzel / Basisklasse
Wurzel / Basisklasse
1. einfache
Vererbung
Vererbung
class abgeleitet : Liste von Basisklassen
Beispiel:
C1
class abgeleitet : virtual (public bzw. private) basis1,
virtual (public bzw. private) basis2 , ...
{...};
C2
2. Vererbung
2. Vererbung
3.16.2 Abstrakte Basisklassen
D
Abstrakte Basisklassen können durch das Definieren mindestens einer Methode als
pure virtual function erzeugt werden.
• Von diesen Klassen kann keine Instanz, also ein Objekt erzeugt werden.
OOM/OOP
91
• Abgeleitete Klassen müssen diese Funktion definieren.
class cGrafischesObjekt
{
virtual int Zeichne(...) = 0;
// pure virtual function
};
3.16.3 Virtuelle Destruktoren
Damit auch die Destruktoren der Basisklassen aufgerufen werden, müssen diese als
virtual definiert sein. Man beachte, dass hier das Konzept des Überschreibens nicht
mehr gilt, da virtuelle Destruktoren aufgerufen werden.
3.16.4 Virtuelle Basisklassen
Durch
Einführung
der
Mehrfachvererbung
entstehen
unter
gewissen
Voraussetzungen Probleme beim Zugriff auf Eigenschaften einer bestimmten
Basisklasse.
Weitere Einzelheiten zu diesem Themengebiet bitte aus der Literatur bzw. Online
Dokumentation eines Compilers entnehmen.
92
OOM/OOP
3.17 Polymorphismus (Späte Bindung)
3.17.1 Frühe Bindung
Kann der Compiler während der Übersetzungszeit sowohl Adresse des Objektes als
auch seinen Typ ermitteln, spricht man von früher Bindung.
z.B.: 2 Möglichkeiten
#include "ratio.h"
#include "iostream.h"
void main(void)
{
ratio r1(1,3),r2(2,7);
cout << “\n direkter Zugriff auf Objekt“;
r2.print();
ratio ∗ratio_zeiger;
ratio_zeiger = &r1;
cout << “\n Indirekter Zugriff auf Objekt“;
ratio_zeiger->print();
}
3.17.2 Späte Bindung
Die späte Bindung stellt erst während der Laufzeit fest, welches Objekt mit welcher
Methode bearbeitet werden soll.
Um anzuzeigen, dass eine Memberfunktion über den late-binding-Mechanismus
aufgerufen werden soll, wird sie in der Basisklasse mit dem Zusatz virtual versehen.
Diese Klassifizierung vererbt sich, braucht also nicht in abgeleiteten Klassen
wiederholt zu werden.
z.B.:
class cKlasse
{
public:
virtual void ausgabe(void) {};
};
OOM/OOP
93
Beispiel:
class cAdresse
{
protected:
char name[50];
char vorname[50];
char straße[30];
char ort[40];
char telefon[20];
public:
virtual int adr_eingeben();
int adr_schreiben();
int adr_listen();
};
spezialisiertere Klasse:
class cKunde : public cAdresse
{
protected:
int kundennummer;
int umsatz[12];
public:
int adr_eingeben();
int lies_umsatz(int monat);
};
mögliche Deklaration:
int cKunde::adr_eingeben()
{
cAdresse::adr_eingeben(); // vgl. scope-resolution// operator
cout << “Kundennummer eingeben: “;
cin >> kundennummer;
}
3.18 Overloading (Überlagerung von Operatoren)
Ein Operatorsymbol, wie “+“ oder “=“, ist nicht auf einen Typ festgelegt. Die
Zuweisung “=“ darf auf int, char, double oder sogar auf Objekte und Strukturen
94
OOM/OOP
angewendet werden. Ähnliches gilt für “+“, mit dem jedoch keine Objekte bearbeitet
werden können.
In C++ gibt es nun die Möglichkeit Operatoren für eigene Klassen zu definieren.
Dazu wird eine Zuordnung Symbol-Typ-Aktion definiert. Als Typ dient eine Klasse
und die Aktion soll eine Methode sein. Der Name der Methode kann mit der
Kombination aus dem Schlüsselwort operator und dem gewünschten Symbol
gebildet werden.
z.B.: Implementierung
ratio ratio::operator+ (ratio op2)
{
…
}
OOM/OOP
95
Beispiel:
→ Datei: ratio.h
#include<stdio.h>
#ifndef
RATIO_H
#define
RATIO_H
class ratio
{
private:
int z;
int n;
// Zähler
// Nenner
public:
ratio(int zaehler = 0, int nenner = 1);
ratio operator+ (ratio &op2);
void print( );
};
#endif // RATIO_H
→ Datei: ratioop.cpp
#include “ratio.h“
#include “iostream.h“
ratio:: ratio(int zae,int ne)
{
z = zae;
n = ne;
}
void ratio::print( )
{
cout << z << “/“ << n;
}
ratio ratio::operator+ (ratio &op2)
{
ratio erg;
erg.z = z ∗ op2.n + n ∗ op2.z;
erg.n = n ∗ op2.n;
return erg;
}
→ Datei: rmainop.cpp
#include<stdio.h>
96
OOM/OOP
#include“ratio.h“
void main(void)
{
ratio A,B(1,2),C(1,4);
A = B + C;
A.print();
}
// Addition
3.19 Friends (Befreundete Funktionen und Klassen)
Das Schlüsselwort friend wird verwendet, um Klassen oder Funktionen die gleichen
Zugriffsrechte wie den Klassenmethoden zu geben.
3.19.1 Befreundete Funktionen
Eine befreundete Funktion ist syntaktisch eine normale Funktion, die jedoch in der
Klassendefinition erwähnt wird und damit die “Zugriffslizenz“ auf die privaten
Elemente erhält. Befreundete Funktionen werden innerhalb der Klasse, auf deren
privaten Elemente sie zugreifen können sollen, deklariert. Das Schlüsselwort friend
teilt dem Compiler mit, dass es sich um eine normale Funktion und nicht um eine
Methode handelt.
→ 3 Arten von Funktionen (insgesamt):
• Normale C-Funktionen
• Befreundete Funktionen
• Methoden
3.19.2 Befreundete Klassen
Neben einzelnen Funktionen können auch ganze Klassen zu Freunden erklärt
werden. Damit erhalten alle Funktionen (Methoden und befreundete Funktionen) der
anderen Klasse die Erlaubnis auf die Elemente der Klasse zuzugreifen.
class cDVKListe
{
friend class cDVKListeIterator;
...
}
// Iterator
OOM/OOP
97
3.20 Templates
Templates werden verwendet um sogenannte parametrisierte Klassen oder
Funktionen zu definieren.
Mit Hilfe von Templates können Klassen generisch erzeugt werden.
Templates werden häufig für container-Klassen (Keller, Stack, Listen) verwendet
(vgl. auch STL). Vererbung ist auch möglich.
98
OOM/OOP
3.20.1 Syntax
Klassen:
template < Argumentliste > class Klassenname
Funktionen:
template < Argumentliste > Typ Funktionsname (Argumentliste)
Argumentliste:
class Bezeichner
Typspezifizierer Bezeichner
3.20.2 Beispiel
Funktionstemplate:
template< class T > void MySwap( T& a, T& b )
{
T c;
c = a; a = b; b = c;
}
Anwendung:
double
int i,
...
MySwap
MySwap
...
x, y;
j;
( x, y );
( i, j );
// Funktion für double wird generiert
// Funktion für int wird generiert
Klassentemplate:
// doppelt verkettete Liste
// forward Deklarationen
template<class Object> class DVKL_Iterator;
// Vorwärts Iterator
template<class Object> class DVKL_IteratorPrev;
// Rückwärts Iterator
template <class Object> class DVKList_ListElem;
// Listenelement
OOM/OOP
99
/* Klasse für DoppeltVerkettete Liste */
template <class Object> class DVKListe : cPHEVCRoot
{
long anzelem; // Anzahl der Listenelemente
DVKList_ListElem<Object> *act, *kopf, *ende;
void DelList(DVKList_ListElem<Object> *);
friend class DVKL_Iterator<Object>;
friend class DVKL_IteratorPrev<Object>;
Object *DeleteDVKList_ListElem();
public:
DVKListe();
~DVKListe();
...
BOOL SetNextPos(void);
...
};
// Bsp. fuer inline Methode
template <class Object> inline BOOL DVKListe<Object>::SetNextPos()
{ return FALSE; }
Anwendung:
typedef DVKListe<cWand> cWandListe;
cWand *pDieWand = new cWand;
cWandListe *WandListe = new cWandListe;
WandListe->Insert(pDieWand);
...
delete pDieWand;
delete WandListe;
3.21 Exception Handling
Unter exception handling versteht man die Behandlung von Ausnahmesituationen
zur Laufzeit eines Programms.
Bei einer Division durch 0 würde ein Programm normalerweise beendet (Zugriff auf
z.B. einen ungültigen Pointer, kann bis zum Systemabsturz führen).
Durch den Einsatz des exception handlings kann das Programm an einer definierten
Stelle weiter ausgeführt werden.
100
OOM/OOP
3.21.1 Syntax
try-block :
try compound-statement handler-list
handler-list :
handler handler-listopt
handler :
catch ( exception-declaration ) compound-statement
exception-declaration :
type-specifier-list declarator
type-specifier-list abstract-declarator
type-specifier-list
...
throw-expression :
throw assignment-expressionopt
Weitere Einzelheiten zu diesem Themengebiet bitte aus der Literatur bzw. Online
Dokumentation eines Compilers entnehmen.
3.21.2 Beispiel
#include <iostream.h>
int main()
{
char *buf;
try
{
buf = new char[512];
if( buf == 0 )
throw "Memory allocation failure!";
}
catch( char * str )
{
cout << "Exception raised: " << str << '\n';
}
return 0;
}
3.22 Run-Time Type Information (RTTI)
OOM/OOP
101
In vielen Fällen ist es notwendig den Typ (die Klasse von der ein Objekt instanziiert
wurde) und die Vererbungshierarchie (z.B. „ist Objekt ein grafisches Objekt“) zu
kennen.
Zu diesem Zweck wurde diese Möglichkeit in die Sprache C++ integriert und gehört
seit kurzer Zeit also zum Sprachumfang.
Mit Hilfe entsprechender Schlüsselwörter und Operatoren
Informationen für jedes Objekt zur Laufzeit abgefragt werden.
können
diese
Wichtig und sinnvoll ist dies, wenn eine Typkonversion vorgenommen werden muss.
Schlüsselwörter:
• The dynamic_cast operator
Used for conversion of polymorphic types.
• The typeid operator.
Used for identifying the exact type of an object.
• The type_info class.
Used to hold the type information returned by the typeid operator.
3.22.1 Syntax
dynamic_cast < type-id > ( expression )
3.22.2 Beispiel
class cGrafischesObjekt { ... };
class cWand : public cGrafischesObjekt { ... };
class cStuetze : public cGrafischesObjekt { ... };
cGrafisches
Objekt
cWand
void f(cGrafischesObjekt *pObj)
{
cWand *pw=0;
cStuetze *ps=0;
pw = dynamic_cast<cWand*>(pObj);
// downcast auf eine Wand
if (pw!=0)
{
pw->Wandfunktion(...);
return;
}
// pObj zeigt nicht auf ein Objekt vom Typ Wand!!!
ps = dynamic_cast<cStuetze*>(pObj);
cStuetze
102
OOM/OOP
// downcast auf eine Stuetze
if (ps!=0)
{
ps->Stuetzenfunktion(...);
return;
}
// pObj zeigt nicht auf ein Objekt vom Typ
// Stuetze!!!
}
Aufruf:
...
cGrafischesObjekt *pGO = new cWand;
...
f(pGO);
...
Weitere Einzelheiten zu diesem Themengebiet bitte aus der Literatur bzw. Online
Dokumentation eines Compilers entnehmen.
3.23 Standard Template Library (STL)
Die Standard Template Library ist eine Bibliothek, die sogenannte container Klassen
in Form von Templates bereitstellt.
Sie gehört mittlerweile zur Standardbibliothek von C++ und muss somit von allen
C++-Compilern unterstützt werden.
Die C++ Standardbibliothek (speziell die STL) wird in Kapitel 5 ausführlich behandelt.
3.24 Name Spaces
Durch das Einführen sogenannter „namespaces“ werden Fehler durch doppelte
Namensvergabe (z.B. Klassennamen (Point), Konstanten (PI) usw.) vermieden.
Beispiel:
// Geobib1.h
class Point { ... };
// Geobib2.h
class Point { ... };
Beim Übersetzen bzw. Linken würde eine Fehlermeldung erzeugt. Man müsste eine
der beiden Klassen umbenennen, was bei einer kommerziellen Bibliothek ohne
sourcecode unmöglich ist.
Die Lösung liegt in der Vergabe eines namespaces:
OOM/OOP
103
// Geobib1.h
namespace Geobib1
{
class Point { ... };
}
// Geobib2.h
namespace Geobib2
{
class Point { ... };
}
// im Modul programm.cpp:
#include "Geobib1.h"
#include "Geobib2.h"
Geobib1::Point p1;
Geobib2::Point p2;
Weitere Einzelheiten zu diesem Themengebiet bitte aus der Literatur bzw. Online
Dokumentation eines Compilers entnehmen.
3.25 Namenskonvention für Bezeichner
Um Programmcode besser lesen und verstehen zu können, sollten bestimmte
Konventionen, die sich an die MFC anlehnen, eingehalten werden.
104
OOM/OOP
3.25.1 Globale Funktionen, Funktionstemplates
Allgemeiner Präfix in Groß-/Kleinschreibung ohne anschließenden Unterstrich zur
Bezeichnung der Bibliothek, der DLL oder des Programms (wie MFC, z.B. AfxFunktionen).
Funktionsbezeichner wie Memberfunktionen.
void CsCreateUndoBuffer( CString strName );
3.25.2 Membervariablen
Mit Präfix "m_".
Elementare Typen erhalten hinter dem Unterstrich einen Typkürzel, der den Datentyp
bezeichnet und sich ggf. aus 2 Teilen zusammensetzt.
Zugriffsart
r: Referenz
p: Pointer (in den Varianten pc und pr, s.u.)
Datentyp
n:
d:
b:
ch:
sz:
str:
(unsigend) int und enum
double
bool
(unsigned) char
(unsigned) char*
CString
Eigentlicher Variablenname
Großbuchstaben beginnen.
in
cWand m_MarkierteObjekte;
int m_nAnzahlObjekte;
Groß-/Kleinschreibung,
jedes
Teilwort
mit
// ohne Typbezeichner
// mit Typbezeichner
Pointer als Datenmembers erhalten zusätzlich einen Buchstaben, der angibt, ob das
Objekt, auf das der Pointer zeigt, zur Klasse selber gehört (d.h. die Klasse ist für
Konstruktion und Freigabe des Objektes zuständig) oder das Objekt nur
"referenziert":
pc: contained, Klasse ist "Besitzer" des Objektes
pr: referenced, Klasse "referenziert" das Objekt
class cWand
{
cGruppe*
cFolie*
m_pcGruppe;
// gehört zur Klasse
m_prActiveFolie;
// referenziert
OOM/OOP
105
};
3.25.3 Layout von header-Dateien
Jede header-Datei ist mit einem Include-Blocker zu versehen, der sich aus dem
Dateinamen folgendermaßen bildet:
Dateiname: z.B. wand.h
Include-Blocker: z.B. WAND_H
#ifndef WAND_H
#define WAND_H
// ......
#endif // WAND_H
3.26 Pointer
Pointer sind Variablen, deren Werte Adressen auf Objekte eines bestimmten
Datentyps darstellen.
Pointerdeklaration:
[Speicherklasse] Typspezifizierer * Variablenname[= Wert];
Wirkung: Es wird eine Pointervariable auf ein Datenobjekt vereinbart.
•
Es gelten die Regeln der Variablendeklaration.
•
Ein Pointer muss vor der Benutzung auf einen Wert (Adresse) initialisiert werden.
Dies kann bei der Deklaration oder zur Laufzeit erfolgen.
•
Mögliche Werte für Pointer sind:
- Adressen von Objekten
- Undefinierte Werte (Pointer ist nicht initialisiert)
- 0 -Wert
Beispiel: *
#include <stdio.h>
void main()
{
int k;
int * adr_k = &k;
*adr_k = 2;
printf("k = %d",k);
/*
/*
/*
/*
int-Variable k */
adr_k erhält Adresse von k */
Dereferenzierung von adr_k */
Ausgabe von k = 2 */
106
OOM/OOP
}
3.26.1 Dynamische Speicherverwaltung
Um Felder und andere Variablen zur Laufzeit des Programms zu erzeugen, gibt es
die Möglichkeit einer dynamischen Speicherverwaltung (in C siehe malloc und free).
In C++ stehen zu diesem Zweck zwei neue Operationen zur Verfügung:
new und delete.
− new besorgt Speicher für Objekte eines Typs. Wenn ausreichend Speicher zur
Verfügung steht, liefert new einen Zeiger auf den Anfang des Bereichs, ansonsten
den Wert 0; new liefert als Typ einen Zeiger auf den angegebenen Typ.
Syntax:
new Typspezifizierer [Größe];
oder
new Typspezifizierer(Initialwert);
int ∗start
int ∗one
int ∗one
=
=
=
new int[20];
new int;
new int(100);
// 20 int-Objekte
// 1 int-Objekt
// mit Initialisierung
− Mit delete kann der Speicher wieder freigegeben werden.
Syntax:
delete [] Variablenname;
oder
delete Variablenname;
delete ∗one;
delete [] start;
// löscht Speicher, auf den Start zeigt
3.26.2 Referenzen
In C++ können Referenzen auf Objekte erzeugt werden. Eine Referenz ist nichts
anderes als ein anderer Name für das gleiche Objekt.
a) Referenzen in Deklarationen
Referenzen in Deklarationen werden durch den &-Operator hinter dem Typ erzeugt.
. Syntax:
OOM/OOP
107
[Speicherklasse] Typspezifizierer & Variablenname = Initialisierer;
Beispiel:
int obj
int &ref_obj
ref_obj
= 1000;
= obj;
= 2000;
// ref_obj ist eine Referenz auf obj
// setzt obj auf 2000
struct sWand w, w2;
struct sWand &ref_w = w;
ref_w.Dicke = 24;
// Zugriff auf Membervariablen mit . !!
ref_w = w2;
// bewirkt ein kopieren von w2 nach w
// ACHTUNG keine neue Referenzzuweisung!!!
Eine Referenz muss immer initialisiert werden und kann anschließend nicht mehr
geändert werden!
b) Referenzen bei Funktionsparametern
→ Referenz-Parameter
Beispiel:
in C:
// Zeiger auf die Variable wird übergeben
void incr (int ∗val)
{
(∗val)++;
}
int x = 1;
incr (&x);
// x = 2
in C++:
void incr (int &val)
// val ist Referenz auf ein Int-Objekt
{
val++;
}
int x = 1;
incr (x);
// x = 2
108
OOM/OOP
− Durch das & wird val zu einer Referenz auf das Objekt, das beim Aufruf an incr( )
übergeben wird.
− Eine Veränderung von val ist gleichbedeutend mit einer Veränderung des
Objektes selbst.
− Das Dereferenzieren mit dem „∗“-Operator entfällt.
OOM/OOP
109
3.27 Felder
Ein Feld (Array) erlaubt es, eine bestimmte Anzahl von gleichen Datenobjekten mit
einem Namen zu bezeichnen. Einzelne Objekte - die Feldelemente - können dann
durch einen Index angesprochen werden. Jedes Element stellt einen L-Wert da.
Felddeklaration:
Speicherklasse Typangabe Bezeichner [Groesse] = {Werte};
Wirkung: Entsprechend der Variablendeklaration wird durch die zusätzliche Angabe
einer Größe die Anzahl der Feldelemente festgelegt.
•
Mögliche Speicherklassen sind auto (Voreinstellung), static und extern.
•
Wird bei der Felddeklaration Speicher reserviert, dann spricht man von einer
Definition.
•
Ist die Speicherklasse statisch, so kann das Feld mit Werten initialisiert werden
(static oder Deklaration außerhalb von Funktionen).
•
In C++ beginnen Felder ab dem Index 0, d.h. der maximale Index beträgt
(Größe – 1).
•
Zur Indizierung von Feldern sind nur Ausdrücke zulässig vom Datentyp int. Die
Typen char, short und long werden in int umgewandelt. Gleitkommatypen (float,
double) sind unzulässig.
•
Die Felder der Speicherklasse automatisch sind explizit zu initialisieren (ggf. zu
Null zu setzen).
•
Der Bezeichner eines Feldes wird als konstanter Pointer des Datentyps auf das
erste Feldelement behandelt.
Beispiel: Felder
#include <stdio.h>
#define MAX_A 10
void main()
{
double a[MAX_A];
int i;
for(i=0 ; i < MAX_A ; i++)
110
OOM/OOP
a[i] = 1.0 + i/10.0;
for(i=0 ; i < MAX_A ; i++)
printf(“Element a[%d] = %lf\n”,i,a[i]);
}
Bild: Feld und Feldelemente
Größe eines Feldes:
sizeof (a) == 80
Größe eines Feldelementes:
sizeof (a[0]) == 8
3.28 Mehrdimensionale Felder
•
Durch Angabe mehrerer Größen in der Deklaration erhält man mehrdimensionale
Felder.
•
Eindimensionale Felder können als Vektoren, mehrdimensionale als Matrizen
aufgefasst werden.
OOM/OOP
111
Beispiel: 2D-Feld
#define N1
#define N2
3
2
void main()
{
int fld[N1][N2],i,j,k = 1;
for(i=0 ; i < N1 ; i++)
for (j=0 ; j < N2 ; j++)
fld[i][j] = k++;
}
Bild: Feld und Feldelemente
3.29 Speicherklassen von Variablen
In C++ werden die Variablen in die Klassen „Automatisch“ und „Statisch“ eingeteilt.
Die Schlüsselwörter auto, static, register und extern bestimmen mit dem Kontext
der Variablendefinition (Ort der Definition) die Speicherklasse.
112
OOM/OOP
3.29.1 Automatische Variablen
•
Der Speicherplatz wird beim Eintreten in dem einer Definition zugeordneten
Anweisungsblock reserviert und beim Verlassen dieses Blockes freigegeben, d.h.
nach Verlassen einer Funktion ist der Variableninhalt verloren.
•
Das Schlüsselwort auto gilt für Definitionen in Blöcken als Voreinstellung und
kann entfallen.
•
Mit dem Schlüsselwort register belegte Variablen werden wenn möglich in den
Registern der CPU abgelegt und bieten somit schnellste Zugriffe auf deren
Inhalte.
Beispiel:
int funktion()
{
auto int a;
int b;
register int
return 0;
}
/*
/*
/*
/*
c;
Deklaration einer int-Funktion
ohne Parameter.
eine automatische int-Variable
entspricht der Deklaration von a
/* Registervariable vom Typ int
*/
*/
*/
*/
*/
3.29.2 Statische Variablen
•
Der Speicherplatz wird für die gesamte Programmlaufzeit reserviert und kann
nicht in Registern abgelegt werden.
•
Die Inhalte bleiben beim Verlassen eines Anweisungsblockes (Funktion) erhalten.
•
Innerhalb eines Anweisungsblockes (Funktion) werden sie mit dem Schlüsselwort
static definiert.
•
Außerhalb von Funktionen definierte Variablen sind immer vom Speichertyp
static.
-
Bei zusätzlicher Angabe des Schlüsselwortes static sind diese Variablen nur
innerhalb der zugehörigen Datei gültig (lokale Gültigkeit).
-
Ohne Angabe von static sind diese Variablen in allen zu einem Programm
gehörenden Dateien gültig (globale Gültigkeit).
OOM/OOP
113
Beispiel:
int i;
/* Statische int-Variable, global gültig,
*/
/* besitzt Gültigkeit im gesamten Programm
*/
static int j; /* Statische int-Variable, lokal gültig */
/* in der Quelltextdatei
*/
int funktion() /* Deklaration einer int-Funktion
*/
{
static int z = 0; /* Statisch int-Variable, gültig*/
/* in der Funktion
*/
z++;
return z;
}
3.29.3 Schlüsselwörter der Speicherklassen
Durch die zusätzliche Angabe von Schlüsselwörtern zu einem Typ wird bei einer
Deklaration die Speicherklasse und der Gültigkeitsbereich von Variablen und
Funktionen bestimmt.
Folgende Anwendungen sind zulässig:
•
Funktionen:
static und extern
•
Variablen:
auto, register, static und extern
3.29.4 Verwendung bei Variablen
•
auto und register
- Deklaration nur zu Beginn eines Anweisungsblockes
- Speicherklasse automatisch
- Deklaration gilt als Definition (Speicherplatz wird reserviert)
- auto entspricht der Voreinstellung und kann entfallen
- register legt Variablen in Registern ab (höhere Geschwindigkeit),
nur für Grundtypen char, short, long und Adresse (Pointer)
•
static
- Deklaration innerhalb von Funktionen (Anweisungsblock) und
außerhalb von Funktionen erlaubt
- Speicherklasse ist statisch
- Deklaration gilt als Definition
- außerhalb von Funktionen wird zusätzlich der Gültigkeitsbereich auf
die Datei beschränkt
114
•
OOM/OOP
extern
- Deklaration innerhalb und außerhalb von Funktionen
- der Speicherplatz der somit deklarierten Variablen ist durch eine
globale Definition woanders erzeugt worden
- dient der Bekanntmachung von Variablen zwischen Funktionen für
den Compiler
•
Voreinstellung innerhalb von Funktionen ist auto mit der Speicherklasse
automatisch, außerhalb von Funktionen die Speicherklasse static.
Beispiele:
extern double a;
static int b;
funktion()
{
extern double c;
register int d;
a = 1.0;
c = a;
return 0;
}
3.29.5 Verwendung bei Funktionen
•
static:
Funktionen sind lokal gültig innerhalb der Datei
•
extern:
Funktionen sind im ganzen Programm gültig
Anwendung in Prototypen (Header-Dateien)
•
Voreinstellung ist extern
3.30 Bibliotheken
Bibliotheken sind Sammlungen kompilierter Funktionen, die bei dem Bindevorgang
nach dem Übersetzen eines Programms zu dem Programm hinzugebunden werden.
In ihnen sind u.a. mathematische (sin, cos, exp), Zeichenketten- (strcpy, strlen) und
Ein-/Ausgabe-Funktionen (printf, scanf, openf) enthalten.
OOM/OOP
115
Benutzung:
Jede Bibliothek besitzt eine Header-Datei, welche in das Programm durch die
Präprozessoranweisung
#include <Headerdateinamen.h>
einzubinden ist.
Diese Header-Datei enthält die für den C-Compiler notwendigen extern
Deklarationen der zu benutzenden Funktionen. Des weiteren sind auch hilfreiche
Konstanten (mathematische Konstanten, Wertebereiche etc.) enthalten.
Nach der include-Anweisung kann die Funktion wie eine selbst geschriebene
Funktion im Programm benutzt werden.
Die Bezeichnungen der Funktionen und der Header-Dateien, der Bibliotheksnamen
(zum Binden) und die Funktionsbeschreibungen (Rückgabewerte und formelle
Parameter) können den Handbüchern des Compilers bzw. der Bibliotheken
entnommen werden.
Beispiel:
#include <stdio.h>
#include <math.h>
#include <string.h>
#define PI 3.141592654
void main(void)
{
printf("sin(PI) = %lf\n",sin(PI));
printf("Laenge = %d\n",strlen("Hallo"));
}
3.31 Präprozessor
Vor dem C-Compiler bearbeitet ein Textformatierer den C-Programmtext, der
Präprozessor. Die Anweisungen für den Präprozessor beginnen alle mit dem
Zeichen #, welches zu Beginn einer Textzeile stehen muss.
Aufgaben des Präprozessors sind die Definition von Konstanten und Makros, die
bedingte Kompilation und das Hereinladen von Textdateien.
116
OOM/OOP
Anweisungen des Präprozessors
#if
#ifdef
#ifndef
#else
#include
#define
#undef
#line
#error
#progma
#endif
Konstanten
Mit der Anweisung
#define Bezeichner Zeichenkette
ersetzt der Präprozessor alle im Text vorkommenden Bezeichner durch die
Zeichenkette.
Beispiel:
#define PI_KONST
#define MAX ANZ
3.1415
10
3.32 Makros
Durch die Definition
#define Bezeichner(Parameter1[,Parameter]) Zeichenkette
wird ein Makro Bezeichner mit einer Parameterliste erzeugt.
Die Zeichenkette kann Ausdrücke mit den Parametern enthalten. Kommt im
Programmtext der Bezeichner vor, werden dessen aktuellen Parameter
entsprechend den formellen Parametern in der Zeichenkette ersetzt und im Text
eingefügt.
OOM/OOP
117
Beispiel:
#define MIN(a,b)
i = MIN(1,3);
ersetzt
(((a) < (b)) ? (a) : (b))
⇒
i = (((1) < (3)) ? (1) : (3));
•
In der Zeichenkette sind die formellen Parameter zu klammern.
falsch:
#define MULT(a,b)
MULT(1,2+3)
->
a * b
1*2+3
richtig:
#define MULT(a,b)
•
(a) * (b)
Nebeneffekte durch Mehrfachauswertung
Beispiel:
MIN(a++,b)
3.33 #include Anweisung
Mit der Anweisung
#include <Dateinamen>
wird eine Datei aus dem Standard-include-Verzeichnis des Compilers in den Text
geladen.
Durch
#include "Dateinamen"
wird die Datei aus dem aktuellen Arbeitsverzeichnis in den Text geladen.
118
OOM/OOP
Im Allgemeinen dienen die #include-Anweisungen zum Hinzuladen von HeaderDateien (Deklarationsdateien) von Bibliotheken oder Header-Dateien eigener
Programme, wenn der Programmtext über mehrere Dateien verteilt wird. Diese
Header-Dateien enthalten Deklarationen für den Präprozessor und den C-Compiler,
welche in den entsprechenden Dateien benötigt werden.
Header-Dateien tragen in C++ die Erweiterung .h im Dateinamen.
3.34 Textdateien
Alle Dateioperationen
Funktionen ausgeführt.
werden
in
C++ durch mehrere
zusammenhängende
Für deren Benutzung ist die Datei stdio.h in das Programm mit
#include <stdio.h>
aufzunehmen.
Eine Textdatei wird in C++ durch einen Zeichenstrom repräsentiert. Dieser
Zeichenstrom wird durch die Funktion fopen() geöffnet. In einem C-Programm wird
der Zeichenstrom dann durch einen von fopen() zurückgegebenen Dateizeiger
angesprochen, welcher ein Pointer vom Typ FILE darstellt.
FILE * fopen(char *dateinamen, char *modus)
Öffnet eine Datei dateinamen mit dem Modus modus
"r"
"w"
"r+"
"w+"
Öffnet eine Textdatei zum Lesen
Anlegen einer Textdatei zum Schreiben
Öffnet eine Textdatei zum Lesen und Schreiben
Anlegen einer Textdatei zum Lesen und Schreiben
fopen() liefert einen Dateizeiger, auf den alle weiteren Operationen bezüglich der
geöffneten Datei durch Funktionen erfolgen.
Wenn sich die Datei nicht öffnen lässt, wird der Wert NULL zurückgeliefert. Nach
jedem Aufruf der Funktion fopen() ist zu prüfen, ob der Dateizeiger ungleich NULL
ist.
OOM/OOP
119
Beispiel:
FILE * fp = fopen(datei,modus);
if(fp == NULL) exit(1);
int fclose(FILE *Dateizeiger)
Schließt die mit dem Dateizeiger verbundene Datei. Vor dem Schließen werden im
Hauptspeicher gepufferte Daten des Zeichenstroms auf die Datei geschrieben. Wird
eine Datei mehrfach geschlossen, tritt ein fataler Laufzeitfehler auf.
int fprintf(FILE *Dateizeiger, char *Format, Argumentenliste)
Pedant zur Funktion printf(), nur dass fprintf() auf den Strom Dateizeiger und nicht
auf den Strom stdout schreibt.
int fscanf(FILE *Dateizeiger, char *Format, Argumentenliste)
Pedant zur Funktion scanf(), nur dass fscanf() aus dem Strom Dateizeiger und
nicht aus dem Strom stdin seine Daten liest.
int rewind(FILE *Dateizeiger)
Positioniert den Dateizeiger auf den Anfang des Zeichenstromes.
Beispiel: Textdateien
#include <stdio.h>
#include <stdlib.h>
void main()
{
int i;
char datei[] ="test.dat";
char buf[100];
FILE *fp;
/* Erzeuge Datei zum Schreiben und Lesen */
fp = fopen(datei,"w+");
/* Pruefe, ob Datei geoeffnet */
if (fp == NULL)
{
printf(„Fehler beim Oeffnen der Datei %s",datei);
exit(1);
}
120
OOM/OOP
/* Ausgabe der Zeichenkette auf Datei */
for(i = 1; i <= 10; i++)
fprintf(fp, "Hallo Welt!");
/* Positioniere Datei auf den Anfang */
rewind(fp);
/* Einlesen der Zeichenkette
Hierbei ist sicherzustellen, dass der Puffer buf
ausreichend groß deklariert ist. */
for(i = 1; i <= 10; i++)
fscanf(fp, "%s",buf);
fclose(fp);
}
3.35 Strukturierte Datentypen
3.35.1 Aufzählungstyp enum
Es kann vorkommen, dass man viele Namen für Werte vergeben will und sich dabei
die Werte aufeinanderfolgender Konstanten nur durch den Wert 1 voneinander
unterscheiden.
1. Möglichkeit:
Folge von define-Instruktionen
→ mühsam und unübersichtlich
2. Möglichkeit:
Typ enum
→ Die Vereinbarung hat die folgende Form:
enum { Namensliste };
→ Form der Elemente der Namensliste:
Namek = Konstantek
oder nur:
Namek
3.36 Beispiel: enum
#define
#define
#define
#define
#define
a0
b5
c 6 äquivalent zu:
d7
e8
enum {a, b = 5, c, d, e}
→ typedef enum { ... } Buchstabe;
OOM/OOP
-
121
Typ der Konstanten muss ganzzahlig sein (char, int, ...).
Name darf kein Schlüsselwort sein.
Wurde nur Namek in der Liste eingegeben, so ist der zugeordnete Wert um 1
höher als der Wert des in der Liste vorausgehenden Elementes.
Geht kein Element voraus, d.h. war Namek als erstes Element der Namensliste
angegeben, so erhält Namek den Wert Null.
Die Reihenfolge der Namen in der enum-Deklaration darf nicht vertauscht
werden (Ausnahme: alle Namen der Liste werden mit einer Wertzuweisung
versehen).
3.36.1 Vereinbarung von Strukturen
Es besteht die Möglichkeit Variablen unterschiedlichen Typs zu einer neuen Einheit
zu verknüpfen. Diese „Verbunde“ werden als Strukturen angegeben, wobei die
Beschreibung der Struktur die folgende allgemeine Form hat:
struct Name
{
Deklarationen;
};
Durch die allgemeine Form wird die Struktur vereinbart, d.h. es wird festgelegt,
welche Variablen und Felder zu der Struktur gehören sollen.
Dabei wird ihr gleichzeitig ein Name gegeben.
Durch
struct Name Variablenliste;
werden Variablen angelegt, die diese Struktur besitzen.
Beispiel: mehrere Merkmale eines Schiffes
struct schiff
{
char *name;
double l, b, t; // Länge, Breite, Tiefgang
int baujahr;
};
struct schiff neu, s[5];
…
122
OOM/OOP
Es werden dabei folgende Speicherbereiche angelegt:
neu
s[0]
s[4]
*name
*name
*name
l
l
l
b
b
t
t
t
baujahr
baujahr
baujahr
. . .
b
3.36.2 Zugriff auf Strukturbereiche
Durch die Angabe des entsprechenden Variablennamens für die Struktur wird als
erstes der gewünschte Speicherbereich adressiert. Nach dem Variablennamen gibt
man – durch einen Dezimalpunkt getrennt – den Namen an wie er innerhalb der
Struktur für eine Variable festgelegt wurde.
Beispiel:
neu.b
// die Variable b im Bereich von neu
und
s[0].b // die gleiche Variable im Bereich von s[0]
Den Variablen innerhalb der Strukturen kann man ebenfalls Werte zuweisen und
zum späteren Zeitpunkt wieder abrufen.
Darüber hinaus kann man eine Zuweisung eines gesamten Strukturbereichs an
einen anderen Strukturbereich vornehmen.
Beispiel:
neu = s[1];
Eine Zeigervariable auf einen Strukturbereich sieht folgendermaßen aus:
Beispiel:
struct schiff *p;
OOM/OOP
123
Durch folgende Zuweisungen wird erreicht, dass die Zeigervariable p auf einen
entsprechenden Speicherbereich verweist:
Beispiel:
p = &neu;
oder:
p = s;
oder:
p = &s[0];
usw.
Über die Punktnotation oder den sogenannten „Struktur-Zeiger-Operator“ kann man
dann wieder auf die Variablen innerhalb einer Struktur zugreifen. Nach der
Zuweisung „p = &neu“ kann z.B. über:
(*p).b bzw. p -> b
(Hinweis: *p.b ist falsch!)
die Variable b angesprochen werden.
Beispiel:
Man möchte z.B. für ein Rettungsboot eines Schiffes Namen, Abmessungen und
Baujahr abspeichern. Die Informationen sollen mit den bereits vorhandenen Angaben
für das Schiff verknüpft werden.
struct schiff
{
char *name;
double l, b, t;
int baujahr;
struct schiff *rett;
};
struct schiff neu, rb;
…
In dem Strukturbereich von neu werden nun die Angaben für das Schiff und in rb die
Angaben für das Rettungsboot gespeichert. Über
neu.rett = &rb;
kann man eine Verknüpfung beider Bereiche vornehmen. Es liegt nun folgende
Situation vor:
124
OOM/OOP
neu
rb
*name
*name
l
l
b
b
t
t
baujahr
baujahr
*rett
*rett
Auf die Informationen des Rettungsbootes rb kann man zusätzlich mit (*neu.rett) z.B.
in der Form (*neu.rett).b oder in Verbindung mit dem Struktur-Zeiger-Operator etwa
in der Form neu.rett -> b zugreifen.
OOM/OOP
125
4 DIE C++-STANDARDBIBLIOTHEK
4.1
•
Allgemeines
Was ist die C++-Standardbibliothek?
Neben der Sprache C++, die sich in relativ kurzer Zeit als De-facto-Standard im
Bereich der objektorientierten Software-Entwicklung etabliert hat, wurde auch eine
dazugehörige Standardbibliothek definiert. Diese wendet die Sprachmittel zwar “nur“
an, erweitert damit aber die Fähigkeiten der Programmierung in dieser Sprache zum
Teil erheblich, ohne die Compiler aufzublähen. Die C++-Standardbibliothek nach
ANSI/ISO-Norm besteht aus den folgenden Teilen:
• Standard Template Library (STL)
→ STL-Container
→ STL-Iteratoren
→ STL-Algorithmen
• Standard-Klassen für Strings
• Standard-Klassen für Bitsets
• Standard-Klassen zur numerischen Datenverarbeitung
• Stream-Klassen zur Ein- und Ausgabe
• Aspekte der Internationalisierung
Im Rahmen der Vorlesung wird jedoch nur auf die Grundlagen der STL
eingegangen. Als weitergehende Literatur wird das Buch “Die C++Standardbibliothek, Nicolai Josuttis, Addison Wesley Verlag“ empfohlen.
•
Was unterscheidet die C++ Standardbibliothek von anderen Bibliotheken?
Ein Hauptteil der C++-Standardbibliothek besteht aus einer Sammlung von
Klassendefinitionen für Standard-Datenstrukturen und einer Sammlung von
Algorithmen, die ganz allgemein für die Bearbeitungen solcher Strukturen verwendet
werden. Dieser Teil der Bibliothek war bisher unter dem Namen Standard Template
Library oder STL bekannt. Organisation und Konzept der STL unterscheiden sich in
nahezu jeder Hinsicht vom Konzept der meisten anderen C++ Bibliotheken, da sie
Kapselung vermeidet und kaum Gebrauch von Vererbung macht.
Die Entwickler der STL haben sich gegen einen völlig objektorientierten Ansatz
entschieden und haben die Aufgaben, die unter Verwendung allgemeiner
Datenstrukturen ausgeführt werden, von der Repräsentation der Strukturen selbst
126
OOM/OOP
getrennt. Aus diesem Grund wird die STL korrekt als eine Sammlung von
Algorithmen und Datenstrukturen angesehen.
•
Wie wirkt sich das nicht objektorientierte Konzept aus?
Der STL-Teil der C++-Standardbibliothek wurde absichtlich in einer nicht objektorientierten Architektur konzipiert. Dieses Konzept hat eine Reihe von Auswirkungen
- einige vorteilhaft, einige weniger vorteilhaft - die Entwickler berücksichtigen
müssen, wenn sie die Bibliothek möglichst effektiv verwenden möchten. Einige
dieser Auswirkungen werden hier erläutert:
-
Schlankerer Quelltext
Es gibt ungefähr fünfzig verschiedene Algorithmen in der STL und etwa ein Dutzend
grundlegende Datenstrukturen. Diese Trennung führt zu einer Verringerung der
Größe des Quelltextes und vermindert zum Teil das Risiko, dass ähnliche Aktivitäten
unterschiedliche Schnittstellen erhalten. Ohne diese Trennung müsste
beispielsweise jeder der Algorithmen in jeder einzelnen der verschiedenen
Datenstrukturen neu implementiert werden, und das würde einige Hundert
Elementfunktionen mehr erfordern, als sich im aktuellen Konzept finden.
-
Flexibilität
Ein Vorteil der Trennung von Algorithmen und Datenstrukturen liegt darin, dass
derartige Algorithmen in Verbindung mit konventionellen C++ Zeigern und Arrays
verwendet werden können. Da C++ Arrays keine Objekte darstellen, verfügen in
einer Klassenhierarchie gekapselte Algorithmen nur selten über diese Fähigkeit.
-
Effizienz
Die STL im besonderen und die C++-Standardbibliothek im allgemeinen stellen einen
Ansatz auf unterer Ebene für die Entwicklung von C++ Anwendungen dar,
sozusagen aus Grundelementen. Dieser Ansatz auf unterer Ebene kann hilfreich
sein, wenn bei bestimmten Programmen die Codegröße und die
Ausführungsgeschwindigkeit einen Schwerpunkt bilden.
•
Iteratoren: Nichtübereinstimmung und Ungültigkeit
Die Datenstrukturen der C++-Standardbibliothek verwenden zeigerähnliche Objekte,
die Iteratoren genannt werden, um den Inhalt eines Containers zu beschreiben.
Aufgrund der Struktur der Bibliothek besteht keine Möglichkeit zu prüfen, ob diese
Iteratorelemente übereinstimmen, d.h. sicherzustellen, dass sie aus dem gleichen
Container abgeleitet sind. Den Anfangsiterator aus einem Container zusammen mit
dem Enditerator aus einem anderen zu verwenden (sei es absichtlich oder zufällig)
ist ein sicherer Weg ins Verderben.
•
Wie wird die C++ Standardbibliothek verwendet?
In einigen Jahren wird die C++-Standardbibliothek die Grundmenge der Klassen und
Bibliotheken darstellen, die zu allen ANSI-konformen C++-Compilern mitgeliefert
OOM/OOP
127
wird. Wir haben bereits darauf hingewiesen, dass das Konzept eines Großteils der
C++-Standardbibliothek nicht objektorientiert ist. Andererseits zeichnet sich C++
gerade durch seine hervorragende Eignung zur Bearbeitung von Objekten aus. Wie
lässt sich also die nicht objektorientierte Struktur der Standardbibliothek mit den
Stärken von C++ in der Bearbeitung von Objekten zusammenführen?
Der entscheidende Punkt besteht in der Verwendung des richtigen Werkzeugs für
jede Arbeit. Die objektorientierten Entwicklungsmethoden und Programmiertechniken
sind nahezu ohne Konkurrenz, wenn es um die Orientierung in großen und
komplexen
Software-Projekten
geht.
Für
die
große
Mehrzahl
der
Programmieraufgaben werden objektorientierte Techniken auch weiterhin das Mittel
der Wahl darstellen.
Setzen Sie die Komponenten der C++-Standardbibliothek direkt ein, wenn es um
Flexibilität und/oder hocheffizienten Programm-Code geht. Verwenden Sie die eher
traditionellen Ansätze der objektorientierten Entwicklung, wie etwa Kapselung und
Vererbung, wenn größere Problembereiche dargestellt werden müssen, und
verbinden Sie dann alle Stücke zu einer umfassenden Gesamtlösung. In Zukunft
werden die meisten Bibliotheken die C++-Standardbibliothek als Grundlage
verwenden. Es ist eine gute Faustregel, den höchsten verfügbaren Kapselungsgrad
zu verwenden, aber stets sicherzustellen, dass die C++-Standardbibliothek als Basis
für die Kommunikation und den Austausch zwischen den einzelnen Bibliotheken zur
Verfügung steht.
Die C++-Standardbibliothek stellt - in Kombination mit traditionelleren Techniken der
objektorientierten Programmierung - ein sehr vielseitig einsetzbares Werkzeug für
jeden dar, der eine Kollektion von C++ Klassen erzeugt, unabhängig davon, ob
solche Klassen als Bibliothek für sich stehen sollen oder ob sie für eine bestimmte
Aufgabe maßgeschneidert werden.
4.2
Die Standard-Template-Library (STL)
Das Herzstück, das die C++-Standardbibliothek stark geprägt hat, ist die StandardTemplate-Library (STL). Diese verwendet den Ansatz, Daten und Algorithmen zu
trennen. Die Daten werden in verschiedenen Arten von Containern gehalten. Die zur
Verfügung stehenden Algorithmen stellen dazu alle möglichen Arten der
Mengenverarbeitung bereit.
Da Datenverarbeitung heutzutage zu einem wesentlichen Teil Mengenverarbeitung
ist und die STL Lösungen für beliebige Arten von Mengen und frei definierbare
Element-Typen zur Verfügung stellt, bekommt C++ damit ein völlig neues
Abstraktionsniveau mit einer erstaunlichen Mächtigkeit.
Einführung
Die STL basiert auf dem Zusammenspiel von mehreren Komponenten:
•
Container dienen dazu, eine Menge von Objekten eines bestimmten Typs zu
verwalten.
•
Iteratoren dienen dazu, über eine Menge von Objekten zu wandern (iterieren).
Dies können insbesondere Elemente aus einem Container sein.
128
•
OOM/OOP
Algorithmen dienen dazu, Mengen als Ganzes und Elemente in den Mengen in
irgendeiner Form zu bearbeiten. Sie können Elemente (um)sortieren, auffinden,
löschen, aber auch modifizieren. Algorithmen verwenden dabei Iteratoren. Damit
müssen die Algorithmen nicht für jede Mengenklasse neu implementiert werden.
Sobald man einen Bereich von Elementen mit Iteratoren durchlaufen kann, kann
man die vordefinierten Algorithmen darauf anwenden.
Algorithmen dienen also zur Bearbeitung der Daten. Iteratoren bilden dabei das
Bindeglied zwischen Daten und Operationen.
Ein wesentlicher Punkt bei dem Konzept liegt in der Tatsache, dass alle
Komponenten nicht auf bestimmte Typen festgelegt sind. Es handelt sich um
Templates (Schablonen), die für beliebige Typen verwendet werden können
(generische Programmierung).
Ergänzt wird das Konzept um Funktionsobjekte und verschiedene Adapter, die die
Mächtigkeit der Algorithmen erweitern oder die Schnittstelle der STL an spezielle
Anforderungen anpassen.
4.3
Container-Klassen
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.
Vektor:
Set/Multiset:
Deque:
Map/Multimap:
Liste:
Abb.: Container der STL
4.3.1 Überblick über Container-Klassen
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 Einfügens
OOM/OOP
129
bestimmt wird. Werden z.B. sechs Elemente nacheinander in eine Menge
jeweils am Ende angehängt, besitzen sie genau die Reihenfolge, in der sie
eingefügt wurden. Vordefinierte sequentielle Container sind Vektoren, Deques
und Listen.
•
Assoziative Container sind dagegen sortierte Mengen, bei denen die Position
der Elemente durch ein Sortierkriterium bestimmt wird. Werden sechs Elemente
nacheinander in eine Menge eingefügt, besitzen sie eine Reihenfolge, die durch
ihren Wert definiert wird. Vordefinierte assoziative Container sind Sets und
Multisets sowie Maps und Multimaps.
Insgesamt stellt die Standardbibliothek nicht weniger als zehn verschiedene
Container-Klassen zur Verfügung. Die folgende Tabelle gibt einen Überblick über die
zehn Container-Typen der Standardbibliothek sowie eine kurze Beschreibung ihrer
wichtigsten Merkmale.
Name
Merkmale
Vector
direkter Zugriff auf Elemente, effiziente Einfügung am Ende
List
effizientes Einfügen und Entfernen an beliebiger Position
Deque
direkter Zugriff, effiziente Einfügung am Anfang oder Ende
Set
Reihenfolge der Elemente wird beibehalten, effizienter Test auf
Inklusion, effiziente Einfügung und Entfernung
Multiset
Set mit mehreren Exemplaren
Map
Zugriff auf Werte über Index, effizientes Einfügen und Entfernen
Multimap
Map, die mehrfach vorhandene Indizes gestattet
Stack
Einfügung und Entfernung nur vom Anfang (oben)
Queue
Einfügung am Ende, Entfernen am Anfang
Priority Queue effizienter Zugriff und Entfernung für größten Wert
4.3.2 Sequentielle Container
•
Vektor
Ein Vektor verwaltet die Elemente in einem dynamischen Array. Er ermöglicht
wahlfreien Zugriff, was bedeutet, dass auf jedes Element direkt zugegriffen
werden kann. Das Anhängen und Löschen von Elementen am Ende des Arrays
ist optimal schnell. Ein Einfügen und Löschen von Elementen mitten im Array
kostet aber Zeit, da dann alle folgenden Elemente entsprechend verschoben
werden müssen.
Beispiel: Vektor mit 6 Elementen füllen und wieder ausgeben
#include <iostream>
#include <vector>
using namespace std;
130
OOM/OOP
void main( )
{
vector<int> menge; // Vektor-Container für int
for(int i = 1; i <= 6; i++)
{
menge.push_back(i);
}
for(int j = 0; j < menge.size( ); j++)
{
cout << menge[j] << endl;
}
}
Mit
vector<int> menge;
wird ein leerer Vektor für Elemente vom Typ int angelegt. Mit der Elementfunktion
push_back( ) wird jeweils ein Element hinten angefügt. Diese Elementfunktion gibt es
für alle sequentiellen Container. Mit der Elementfunktion size( ) wird die Anzahl von
Elementen im Container abgefragt. Diese Funktion ist für alle Container definiert.
•
Deque
Eine Deque ist ein dynamisches Array, das so implementiert ist, dass es in beide
Richtungen wachsen kann. Insofern geht das Einfügen und Löschen von
Elementen nicht nur am Ende, sondern auch am Anfang optimal schnell. Aber
auch hier dauern Änderungen mitten in der Menge ihre Zeit. Beim Einfügen wird
neben der Elementfunktion push_back( ) die Elementfunktion push_front( )
verwendet.
•
Liste
Eine Liste ist als doppelt verkettete Liste von Elementen implementiert. Das
bedeutet, dass jedes Element in der Menge auf seinen Vorgänger und seinen
Nachfolger zeigt. Dadurch ist kein wahlfreier Zugriff mehr möglich. Um z.B. auf
das zehnte Element zuzugreifen, müssen erst die ersten neun aufgesucht
werden. Ein Wechsel auf benachbarte Elemente ist in beiden Richtungen in
konstanter Zeit möglich. Das Einfügen und Löschen von Elementen an allen
Stellen ist gleich schnell. Es müssen lediglich die entsprechenden Zeiger
umgehängt werden.
4.3.3 Assoziative Container
OOM/OOP
131
Assoziative Container sortieren die Elemente automatisch anhand eines bestimmten
Ordnungskriteriums. Dieses Ordnungskriterium besteht aus einer Vergleichsfunktion,
die entweder den Wert der Elemente oder einen dazugehörigen Schlüssel-Wert
verarbeitet. Die Vergleichsfunktion kann dabei selbst definiert werden. Bei allen
assoziativen Container-Klassen der STL ist zu beachten, dass als zweites TemplateArgument optional die Sortierfunktion übergeben werden kann. Wird sie nicht
übergeben, wird mit dem “kleiner als“-Operator sortiert.
•
Set
Ein Set-Container ist eine Menge, bei denen die Elemente nur jeweils einmal
vorkommen dürfen und automatisch nach ihrem Wert sortiert werden.
•
Multiset
Ein Multiset-Container entspricht einem Set mit dem Unterschied, dass Elemente
mit gleichem Wert mehrfach vorkommen dürfen. Auch hier werden sie
automatisch nach ihrem Wert sortiert.
•
Map
Ein Map-Container verwaltet Schlüssel/Werte-Paare. Zu jedem Element gehört
ein identifizierender Schlüssel, nach dem sortiert wird, und ein dazugehöriger
Wert. Jeder Schlüssel darf hier nur einmal vorkommen.
•
Multimap
Ein Multimap-Container entspricht einer Map mit dem Unterschied, dass die
Schlüssel mehrfach vorkommen dürfen.
4.4
Container-Adapter
Neben den fundamentalen Container-Klassen existieren noch spezielle ContainerAdapter, die die fundamentalen Container auf spezielle Anforderungen abbilden.
Vordefiniert sind:
-
Stack
Ein Stack ist ein Stapel/Keller von Elementen eines bestimmten Typs.
-
Queue
Eine Queue ist ein Puffer von Elementen eines bestimmten Typs.
-
Priority Queue
Eine Priority Queue ist eine Queue, bei der die Elemente nach einer bestimmten
Priorität ausgelesen werden. Dabei kann das Sortierkriterium wieder übergeben
werden.
Container-Adapter sind zwar mit der STL in die Standardbibliothek aufgenommen
worden, für die Arbeit mit Iteratoren und Algorithmen stehen sie aber nicht zur
Verfügung.
132
•
OOM/OOP
Wahl des geeigneten Containers
Die folgenden Fragen helfen herauszufinden, welcher Container-Typ sich für die
Lösung eines bestimmten Problems am besten eignet.
•
Wie soll auf Werte zugegriffen werden?
Wenn direkter Zugriff wichtig ist, sollte ein Vektor oder eine Deque verwendet
werden. Wenn sequentieller Zugriff ausreicht, kann eine der anderen
Datenstrukturen geeignet sein.
•
Ist die Reihenfolge, in der Werte in die Kollektion gehalten werden, wichtig?
Es gibt eine Reihe verschiedener Verfahren, um Werte zu sequenzieren. Wenn
während der gesamten Lebensdauer des Containers eine strikte Beachtung der
Reihenfolge erforderlich ist, dann ist die Datenstruktur Set eine naheliegende Wahl,
denn in ein Set eingefügte Elemente erhalten automatisch den korrekten Platz in der
Reihenfolge. Wenn andererseits diese Reihenfolge nur an einem bestimmten Punkt
wichtig ist (beispielsweise am Ende einer langen Reihe von Einfügevorgängen),
kann es einfacher sein, die Werte in eine Liste oder einen Vektor einzusetzen und
die sich ergebende Struktur zu gegebener Zeit zu sortieren. Wenn die Reihenfolge
innerhalb der Struktur mit der Reihenfolge der Einfügungen zusammenhängt, dann
können ein Stack, eine Queue oder eine Liste die beste Wahl sein.
•
Ändert sich die Größe der Struktur während der Ausführung stark?
Falls dies zutrifft, könnten eine Liste oder ein Set die beste Wahl sein. Ein Vektor
oder Deque wird auch, nachdem Elemente entfernt wurden, einen großen Puffer
verwalten. Wenn umgekehrt die Größe einer Kollektion relativ gering schwankt,
benötigt ein Vektor oder Deque weniger Arbeitsspeicher als eine Liste oder ein Set
mit der gleichen Anzahl Elemente.
•
Kann die Größe der Kollektion abgeschätzt werden?
Die Datenstruktur Vector (Vektor) bietet die Möglichkeit, vorab einen Speicherblock
von festgelegter Größe zu reservieren (mit der Elementfunktion reserve()). Diese
Möglichkeit besteht bei den anderen Containern nicht.
•
Muss häufig auf Einbettung von Werten in der Kollektion getestet werden?
Trifft dies zu, sind die Container Set oder Map eine gute Wahl. Die Prüfung, ob ein
Wert in einem Set oder einer Map enthalten ist, lässt sich in sehr wenigen Schritten
durchführen (logarithmisch zur Größe des Containers). Hingegen kann die Prüfung,
ob ein bestimmter Wert in einem der anderen Kollektionstypen enthalten ist, den
Vergleich des Wertes mit jedem Element, das sich in dem Container befindet,
erfordern.
OOM/OOP
•
133
Ist die Kollektion indiziert? Kann die Kollektion als eine Folge von
Index/Wert-Paaren gedacht werden?
Wenn die Indizes Ganzzahlen zwischen 0 und einer Obergrenze sind, sollten ein
Vektor oder Deque verwendet werden. Wenn die Indexwerte dagegen einem
anderen sortierten Datentyp entsprechen (wie etwa Zeichen, Strings oder ein
benutzerdefinierter Typ), kann der Container-Typ Map verwendet werden.
•
Können die Werte zueinander in Beziehung gesetzt werden?
Alle Werte, die in einem der Container der Standardbibliothek gespeichert werden
können, müssen auf Gleichheit mit einem anderen Wert überprüft werden können.
Es müssen jedoch nicht alle mit dem relationalen Kleiner-als-Operator verglichen
werden können. Wenn aber Werte nicht mit dem relationalen Kleiner-als-Operator
geordnet werden können, können sie nicht in einem Set oder einer Map gespeichert
werden.
•
Muss häufig der größte Wert der Kollektion gefunden oder entfernt werden?
Trifft dies zu, ist die Datenstruktur der Ereigniswarteschlange die beste Wahl.
•
An welchen Positionen sollen Werte in die Struktur eingefügt oder aus ihr
entfernt werden?
Wenn Werte an beliebiger Position eingefügt werden sollen, ist eine Liste die beste
Wahl. Wenn Werte nur am Anfang eingefügt werden müssen, sind Deque oder Liste
vorzuziehen. Wenn Werte nur am Ende eingefügt oder entfernt werden, sind Stack
oder Queue eine naheliegende Wahl.
•
Müssen häufig zwei oder mehr Sequenzen zu einer zusammengeführt
werden?
Trifft dies zu, scheinen sich Set oder Liste anzubieten, je nachdem, ob die
Reihenfolge der Kollektion gewahrt bleiben muss. Das Zusammenführen zweier
Sets ist ein sehr effizienter Vorgang. Wenn die Kollektionen keine Reihenfolge
haben, aber die effektive Elementfunktion splice() aus der Klasse List verwendet
werden kann, ist der Datentyp List vorzuziehen, da diese Operation in den anderen
Containern nicht zur Verfügung steht.
In vielen Fällen bieten sich mehrere verschiedene Container für die Lösung eines
gegebenen Problems an. In diesem Fall ist eine Möglichkeit, die tatsächlichen
Ausführungszeiten zu vergleichen, um die beste Alternative herauszufinden.
134
4.5
OOM/OOP
Gemeinsame Operationen
Ausdruck
Bedeutung
ContTyp m
ContTyp m1 (m2)
erzeugt einen leeren Container ohne Elemente
erzeugt einen Container als Kopie eines anderen
ContTyp m (anf, end)
erzeugt einen Container und initialisiert ihn mit
Kopien der Elemente aus dem Bereich (anf, end)
m.~ContTyp ( )
löscht alle Elemente und gibt den Speicherplatz
frei
m.size ( )
m.empty ( )
liefert die aktuelle Anzahl von Elementen
liefert, ob der Container leer ist
(entspricht: size ( ) == 0 )
liefert die maximal mögliche Anzahl von
Elementen
liefert, ob m1 gleich m2 ist
liefert, ob m1 ungleich m2 ist
(entspricht: !(m1==m2) )
liefert, ob m1 kleiner als m2 ist
liefert, ob m1 größer als m2 ist
(entspricht: m2<m1 )
liefert, ob m1 kleiner oder gleich m2 ist
(entspricht: !(m2<m1) )
liefert, ob m1 größer oder gleich m2 ist
(entspricht: !(m1<m2) )
weist m1 alle Elemente von m2 zu
vertauscht die Elemente von m1 mit denen von
m2
vertauscht die Elemente von m1 mit denen von
m2 (als globale Funktion)
m.max_size ( )
m1 == m2
m1 != m2
m1 < m2
m1 > m2
m1 <= m2
m1 >= m2
m1 = m2
m1.swap (m2)
swap (m1, m2)
m.begin ( )
m.end ( )
m.rbegin ( )
m.rend ( )
liefert einen Iterator für das erste Element
liefert einen Iterator für die Position hinter dem
letzten Element
liefert einen Reverse-Iterator für das erste
Element eines umgekehrten Durchlaufs (also das
letzte Element)
liefert einen Reverse-Iterator für die Position
hinter dem letzten Element eines umgekehrten
Durchlaufs (also die Position vor dem ersten
Element)
m.insert (pos, elem)
fügt eine Kopie von elem ein (Rückgabewert und
die Bedeutung von pos sind unterschiedlich)
m.erase (pos)
m.erase (anf, end)
löscht das Element an der Iterator-Position pos
löscht alle Elemente des Teilbereichs (anf,end)
und liefert die Position des Folge-Elements
löscht alle Elemente (leert den Container)
m.clear ( )
OOM/OOP
4.6
135
Iteratoren
4.6.1 Allgemeines
Iteratoren sind zeigerähnliche Objekte, die verwendet werden, um auf alle Objekte,
die sich in einem Container befinden, der Reihe nach zuzugreifen.
Das Konzept des Iterators ist grundlegend für den Gebrauch der Container-Klassen
und der mit ihnen verbundenen Algorithmen, die von der Standardbibliothek zur
Verfügung gestellt werden. Abstrakt betrachtet ist ein Iterator einfach ein dem Zeiger
ähnliches Objekt, das verwendet wird, um reihum auf alle Elemente, die in einem
Container gespeichert sind, zuzugreifen. Da unterschiedliche Algorithmen Container
auf mehrere verschiedene Arten durchlaufen müssen, gibt es verschiedene Arten
von Iteratoren. Jede Container-Klasse in der Standardbibliothek kann einen Iterator
mit der Funktionalität erzeugen, die der bei der Implementierung des Containers
verwendeten Speichertechnik angemessen ist. In der Hauptsache bestimmt die
Kategorie der als Argument erforderlichen Iteratoren, welche Algorithmen der
Standardbibliothek zusammen mit welchen Container-Klassen verwendet werden
können.
Ein Wertebereich ist eine Folge von Werten in einem Container. Der Wertebereich
wird durch ein Iteratorpaar bezeichnet, das Anfang und Ende der Sequenz definiert.
So wie Zeiger in der traditionellen Programmierung auf verschiedene Weise
verwendet werden können, lassen sich auch Iteratoren für eine Reihe verschiedener
Zwecke einsetzen. Ein Iterator kann verwendet werden, um einen bestimmten Wert
zu bezeichnen, genauso wie ein Zeiger benutzt werden kann, um eine bestimmte
Speicherstelle zu referenzieren. Andererseits kann ein Iteratorpaar eingesetzt
werden, um einen Wertebereich von Werten zu bezeichnen, analog zu der Art, in
der zwei Zeiger einen zusammenhängenden Speicherbereich angeben können. Im
Fall der Iteratoren folgen aber die so bezeichneten Werte nicht mit Notwendigkeit
auch physikalisch aufeinander; vielmehr ist die Reihenfolge eine logische, weil sie
aus dem gleichen Container abgeleitet sind, und der zweite Wert auf den ersten in
der Reihenfolge folgt, in der die Elemente vom Container gehalten werden.
Wenn in einem C++ Programm zwei Zeiger zum Bezeichnen eines
Speicherbereichs verwendet werden, wird gemäß Konvention der Endzeiger nicht
als Teil des bezeichneten Bereichs angesehen. Man kann z.B. von einem Array mit
dem Namen x und einer Länge von 10 auch sagen, er erstrecke sich von x bis x+10,
obwohl das Element an der Position x+10 nicht Bestandteil des Arrays ist. Vielmehr
ist der Zeigerwert x+10 der Nach-Bereichsende-Wert - das Element, das den
nächsten Wert nach dem Ende des bezeichneten Bereichs hat. Iteratoren werden
zum Beschreiben von Bereichen auf gleiche Weise verwendet. Der zweite Wert wird
nicht als Teil des bezeichneten Bereichs angesehen. Vielmehr ist der zweite Wert
der Nach-Bereichsende-Wert, der den in der Folge nächsten Wert nach dem letzten
Wert des Bereichs bezeichnet.
136
OOM/OOP
Drei fundamentale Operationen definieren das Verhalten eines Iterators:
•
Iterator::operator∗( )
Liefert das Element, an dessen Position sich der Iterator befindet.
•
Iterator::operator++( )
Setzt den Iterator ein Element weiter
•
Iterator::operator==( )
Liefert, ob zwei Iteratoren dasselbe Objekt repräsentieren (gleicher Container,
gleiche Position). Natürlich ist auch der entsprechende Test auf Ungleichheit
definiert.
Um mit Iteratoren arbeiten zu können, stellen
Elementfunktionen bereit. Die beiden wichtigsten sind:
•
Container
entsprechende
Container::begin( )
Liefert einen Iterator, der die Position des ersten Elements im Container
repräsentiert.
•
Container::end( )
Liefert einen Iterator, der die Position hinter dem letzten Element im Container
repräsentiert.
begin ()
end ()
Abb.: begin() und end() bei Containern
Der Bereich von begin() bis end() ist eine halboffene Menge. Dies hat den Vorteil,
dass sich eine einfache Ende-Bedingung für Iteratoren, die über Container wandern,
definieren lässt: Die Iteratoren wandern, bis end( ) erreicht ist. Ist begin() gleich
end(), ist die Menge leer.
4.6.2 Beispiele
Beispiel 1: Elemente ‘a‘ bis ‘z‘ in eine List einfügen und wieder ausgeben
(sequentieller Container)
#include <iostream>
#include <list>
#include <iterator> // kann meistens entfallen
OOM/OOP
137
using namespace std;
void main(void)
{
list<char> menge;
for (char c = ‘a‘; c <= ‘z‘; c++)
{
menge.push_back(c);
}
list<char>::iterator pos;
for (pos = menge.begin(); pos != menge.end(); pos++)
{
cout << ∗pos << endl;
}
}
Beispiel 2: Elemente 1 bis 6 durcheinander in ein Set einfügen und absteigend
sortiert ausgeben (assoziativer Container)
#include <iostream>
#include <set>
using namespace std;
void main(void)
{
typedef set<int,greater<int> > IntSet;
// Leerzeichen beachten!
IntSet menge;
menge.insert
menge.insert
menge.insert
menge.insert
menge.insert
menge.insert
(3);
(1);
(5);
(4);
(6);
(2);
IntSet::iterator pos;
for (pos = menge.begin(); pos != menge.end(); pos++)
{
cout << ∗pos << endl;
}
}
Hier wird als Sortierkriterium greater<int> übergeben. Dies bewirkt, dass die
Elemente mit > absteigend sortiert werden. Greater ist ein vordefiniertes
Funktionsobjekt. Wird kein Sortierkriterium übergeben, wird als Default aufsteigend
138
OOM/OOP
sortiert. Mit der Elementfunktion insert() wird jeweils ein Element eingefügt und an
die richtige Stelle einsortiert.
Beispiel 3: Map mit strings als Schlüssel und floats als Werte (absteigend sortiert)
#include <iostream>
#include <map>
#include <string>
using namespace std;
void main(void)
{
typedef map<string,float,greater<string> >
StringFloatMap;
StringFloatMap menge;
menge["Tag“] = 7;
menge["Monat"] = 1;
menge["Jahr"] = 1999;
StringFloatMap::iterator pos;
for(pos = menge.begin(); pos != menge.end(); ++pos)
{
cout << "key: " << pos->first << " " << "Value: "
<< pos->second << endl;
}
}
4.6.3 Kategorisierung von Iteratoren
Für die vordefinierten Container-Klassen existieren folgende zwei Kategorien von
Iteratoren:
•
Bidirectional-Iteratoren
Bei Bidirectional-Iteratoren ist es möglich, bidirectional, also in zwei Richtungen
zu iterieren: vorwärts mit dem Inkrement-Operator und rückwärts mit dem
Dekrement-Operator. Dazu gehören die Iteratoren der Container-Klassen list,
set, multiset, map und multimap.
•
Random-Acess-Iteratoren
Random-Access-Iteratoren sind Iteratoren mit wahlfreiem Zugriff. Diese
Iteratoren beherrschen neben allen Fähigkeiten von Bidirectional-Iteratoren
zusätzlich die Operationen, die durch den wahlfreien Zugriff möglich sind. Dazu
gehören insbesondere alle Operatoren für “Adreß-Arithmetik“: Das Bilden von
OOM/OOP
139
Differenzen, das Addieren und Subtrahieren von Offsets sowie die Vergleiche mit
“kleiner als“ und “größer als“.
4.7
Algorithmen
4.7.1 Allgemeines
Es existieren zahlreiche Standard-Algorithmen, mit denen Mengen und deren
Elemente bearbeitet werden können. Dazu gehören u.a. Algorithmen zum Finden,
Vertauschen, Sortieren, Kopieren, Aufaddieren und Modifizieren von Elementen.
Diese Algorithmen sind keine Elementfunktionen der Container-Klassen, sondern
globale Funktionen, die mit Iteratoren arbeiten. So muss jeder Algorithmus nur
einmal implementiert werden. Außerdem können so auch selbstdefinierte
Mengenklassen bearbeitet werden.
4.7.2 Beispiel:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void main(void)
{
vector<int> menge;
// Elemente 1 bis 6 werden unsortiert in einen Vektor
// eingefügt
menge.push_back (3);
menge.push_back (1);
menge.push_back (5);
menge.push_back (4);
menge.push_back (6);
menge.push_back (2);
// kleinstes und groesstes Element ausgeben
vector<int>::iterator pos;
pos = min_element(menge.begin(),menge.end());
cout << “min: “ << ∗pos << endl;
pos = max_element(menge.begin(),menge.end());
cout << “max: “ << ∗pos << endl;
// alle Elemente aufsteigend sortieren
sort(menge.begin(),menge.end());
// Reihenfolge vom zweiten bis vorletzten Element
140
OOM/OOP
// umdrehen
reverse(menge.begin()+1,menge.end()-1);
// alle Elemente ausgeben
for (int i=0;i<menge.size();i++)
{
cout << menge[i] << ‘ ‘;
}
cout << endl;
}
Ausgabe:
min: 1
max: 6
154326
4.7.3 Bereiche in Algorithmen
Alle Algorithmen bearbeiten einen oder mehrere Bereiche von Elementen. Dieser
kann alle Elemente eines Containers umfassen, aber auch eine Teilmenge sein.
Der Anwender eines Algorithmus muss selbst darauf achten, dass das übergebene
Ende eines Bereichs vom Anfang aus erreichbar (reachable) ist. Die Iteratoren, die
den Bereich definieren, müssen immer zum gleichen Container gehören, und die
Endposition darf nicht vor der Anfangs-Position stehen. Ist dies nicht der Fall, ist das
Verhalten i.a. undefiniert, was in der Praxis zu Endlosschleifen oder unerlaubten
Speicherzugriffen führen kann.
Bei den meisten Algorithmen, die mehrere Bereiche bearbeiten, muss nur beim
ersten Bereich sowohl der Anfang als auch das Ende angegeben werden. Bei allen
anderen Bereichen reicht die Angabe des Anfangs. Das Ende folgt dann aus dem
Zusammenhang bzw. der Operation. Dies gilt insbesondere bei Algorithmen, die
Elemente ggf. modifiziert in eine andere Menge kopieren. Es ist unbedingt darauf zu
achten, dass Zielmengen groß genug sind! Damit der Zielbereich groß genug ist,
muss er entweder gleich mit der richtigen Größe angelegt oder explizit auf die
richtige Größe gesetzt werden. Beides ist jedoch nur bei sequentiellen Containern
möglich.
4.7.4 Beispiel:
#include <iostream>
#include <vector>
#include <list>
#include <deque>
#include <algorithm>
using namespace std;
void main(void)
OOM/OOP
141
{
list<int> menge1;
vector<int> menge2;
// Elemente 1 bis 9 in die erste Menge einfügen
for (int i = 1; i <= 9; i++)
{
menge1.push_back(i);
}
/*************************************************************
Elemente in die zweite Menge kopieren
Laufzeitfehler:
copy(menge1.begin(),menge1.end(),
<- Quellbereich
menge2.begin());
<- Zielbereich
*************************************************************/
// Platz für die zu kopierenden Elemente schaffen
menge2.resize(menge1.size());
// Elemente in die zweite Menge kopieren
copy(menge1.begin(),menge1.end(), menge2.begin());
// dritte Menge ausreichend groß definieren
deque<int> menge3(menge1.size());
// Elemente in die dritte Menge kopieren
copy(menge1.begin(),menge1.end(), menge3.begin());
}
4.7.5 Funktionen als Parameter von Algorithmen
Zahlreiche Algorithmen erhalten als Parameter eine Funktion, die dann intern
aufgerufen wird. Das einfachste Beispiel ist der Algorithmus for_each(), der für jedes
Element eine übergebene Funktion aufruft.
Beispiel:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void print(int elem)
{
cout << elem << endl;
142
OOM/OOP
}
void main(void)
{
vector<int> menge;
// Elemente 1 bis 6 in die Menge einfügen
for (int i = 1; i <= 6; i++)
{
menge.push_back(i);
}
// alle Elemente ausgeben
for_each(menge.begin(), menge.end(), print);
}
4.7.6 Alle Algorithmen im Überblick
•
Nicht-modifizierende Algorithmen
Die nicht-modifizierenden Algorithmen ändern weder die Reihenfolge noch den
Wert der Elemente, für die sie aufgerufen werden.
Name
for_each ( )
find ( ), find_if ( )
search ( )
find_end ( )
find_first_of ( )
adjacent_find ( )
min_element ( )
max_element ( )
count ( ), count_if ( )
equal ( )
lexicographical_compare ( )
mismatch ( )
•
Funktionalität
ruft für jedes Element eine Read-OnlyOperation auf
sucht bestimmtes Element
sucht erste Teilfolge von bestimmten
Werten
sucht letzte Teilfolge von bestimmten
Werten
sucht eines von mehreren möglichen
Elementen
sucht zwei benachbarte Elemente mit
bestimmten Eigenschaften
liefert das kleinste Element
liefert das größte Element
zählt die Elemente
testet zwei Bereiche auf Gleichheit
testet zwei Bereiche lexikalisch auf <
liefert die beiden ersten unterschiedlichen Elemente zweier Bereiche
Modifizierende Algorithmen
Modifizierende Algorithmen verändern den Wert von Elementen. Dies kann für
den bearbeiteten Bereich oder einen Zielbereich des Algorithmus gelten.
OOM/OOP
Name
copy ( )
copy _backward ( )
swap_ranges ( )
transform ( )
fill ( ), fill_n ( )
generate ( ), generate_n ( )
replace ( ), replace_if ( )
replace_copy ( ), replace_copy_if ( )
•
143
Funktionalität
kopiert einen Bereich
kopiert einen Bereich angefangen von
hinten
vertauscht die Elemente zweier Bereiche
modifiziert die Elemente eines oder
zweier Bereiche
gibt mehreren Elementen einen festen
Wert
gibt mehreren Elementen einen jeweils
generierten Wert
ersetzt bestimmte Werte durch einen
festen neuen Wert
kopiert einen Bereich und ersetzt dabei
Elemente
Löschende Algorithmen
Löschende Algorithmen entfernen Elemente aus einem Bereich. Dies betrifft
entweder den bearbeiteten Bereich oder einen Zielbereich, in den die nicht
entfernten Elemente kopiert werden.
Name
remove ( ), remove_if ( )
remove_copy ( ), remove_copy_if ( )
Funktionalität
löscht bestimmte Elemente
kopiert einen Bereich und löscht
dabei Elemente
unique ( )
löscht aufeinanderfolgende
Duplikate
unique_copy ( )
kopiert und löscht dabei
aufeinanderfolgende Duplikate
144
•
Mutierende Algorithmen
Mutierende Algorithmen verändern die Reihenfolge von Elementen, ohne deren
Werte zu ändern.
Name
reverse ( )
Funktionalität
kehrt die Reihenfolge von Elementen um
reverse_copy ( )
kopiert und kehrt die Reihenfolge der
Elemente um
rotiert die Elemente
kopiert und rotiert dabei die Elemente
permutiert die Reihenfolge in eine
Richtung
permutiert die Reihenfolge in die andere
Richtung
bringt die Reihenfolge der Elemente
durcheinander
verschiebt bestimmte Elemente nach
vorne
verschiebt bestimmte Elemente nach
vorne und behält dabei relative
Reihenfolgen
rotate ( )
rotate_copy ( )
next_permutation ( )
prev_permutation ( )
random_shuffle ( )
partition ( )
stable_partition ( )
•
Sortierende Algorithmen
Name
sort ( )
stable_sort ( )
partial_sort ( )
partial_sort_copy ( )
nth_element ( )
make_heap ( )
push_heap ( )
pop_heap ( )
sort_heap ( )
•
OOM/OOP
Algorithmen für sortierte Bereiche
Name
lower_bound ( )
upper_bound ( )
equal_range ( )
binary_search ( )
includes ( )
Funktionalität
sortiert Elemente
sortiert Elemente unter Beibehaltung der
Reihenfolge von Elementen mit gleichem
Wert
sortiert die ersten n Elemente
kopiert die ersten n sortierten Elemente
sortiert in Hinsicht auf ein bestimmtes
Element
macht einen Bereich zu einem Heap
integriert ein Element in einen Heap
löst ein Element aus einem Heap
sortiert einen Heap komplett
Funktionalität
liefert das erste Element >= einem Wert
liefert das erste Element > einem Wert
liefert einen Bereich von Elementen mit
einem bestimmten Wert
liefert, ob ein Element enthalten ist
liefert, ob alle Elemente einer Teilmenge
enthalten sind
OOM/OOP
merge ( )
set_union ( )
set_intersection ( )
set_difference ( )
set_symmetric_difference ( )
inplace_merge ( )
•
faßt die Elemente zweier Bereiche
zusammen
bildet die Vereinigungsmenge zweier
Bereiche
bildet die Schnittmenge zweier Bereiche
bildet die Differenzmenge zweier
Bereiche
bildet die Komplementärmenge zweier
Bereiche
verschmilzt zwei direkt hintereinander
liegende Teilbereiche
Numerische Algorithmen
Die numerischen Algorithmen dienen dazu, die Elemente eines Bereichs in
verschiedener Form numerisch miteinander zu verknüpfen.
Name
accumulate ( )
partial_sum ( )
adjacent_difference ( )
inner_product ( )
4.8
145
Funktionalität
verknüpft alle Elemente
verknüpft ein Element jeweils mit allen
Vorgängern
verknüpft jeweils ein Element mit seinem
Vorgänger
verknüpft alle Verknüpfungen von jeweils
zwei Elementen zweier Bereiche
Fehlerbehandlung in der STL
Bei der Verwendung der STL finden keinerlei Überprüfungen statt, ob
Bereichsgrenzen stimmen, Iteratoren zum gleichen Container gehören, ob auf ein
nicht vorhandenes Element zugegriffen wird und so weiter.
Wenn ein Fehler passiert, ist das Verhalten des Programms undefiniert. Konkret
sollte auf folgende Fehler geachtet werden:
•
Iteratoren müssen initialisiert worden sein.
•
Iteratoren müssen einen definierten Wert besitzen.
•
Bei Bereichen müssen beide Iteratoren zum gleichen Container gehören und der
Iterator, mit dem der Bereich beginnt, muss vor dem Iterator stehen, mit dem der
Bereich endet. Der zweite muss also vom ersten erreichbar (reachable) sein.
•
Zur Ende-Position gehört kein Element, weshalb auch nicht versucht werden darf
darauf zuzugreifen.
•
Werden mehrere Quellbereiche verwendet, müssen alle Bereiche mindestens so
groß sein wie der erste.
•
Zielbereiche müssen groß genug sein, oder es müssen einfügende Iteratoren
verwendet werden.
146
OOM/OOP
5 Die Microsoft Foundation Class Library
5.1
Grundlagen
Die MFC ist die Basis der Windows-Programmierung unter Visual C++. Sie ist
sozusagen das Abbild der Windows-Struktur in dieser Programmiersprache (und
noch ein wenig mehr). Mit der MFC steht einerseits ein fertiges Programmgerüst und
andererseits eine Schnittstelle zu Windows bereit.
Windows stellt über eine einheitliche Schnittstelle API (Application Programming
Interface) seine Funktionalität zur Verfügung. Ein direktes Programmieren der APIFunktionen ist äußerst aufwendig und fehleranfällig. Die verschiedenen
Programmiersprachen verstecken daher hinter eigenständigen Oberflächen (sog.
Shells) die API-Funktionen, um sie indirekt aufzurufen. Diese Oberflächen sind sehr
viel einfacher zu bedienen als die Programmierung mit API-Funktionen.
Wie der Name MFC schon sagt, ist sie eine Sammlung von Klassen, genauer
mehrerer Klassenhierarchien. Der Begriff der Klassenhierarchie stammt aus der
OOP (objektorientierten Programmierung) und sagt aus, dass die MFC völlig
objektorientiert aufgebaut ist. Microsoft hat im Laufe der Zeit die MFC geändert und
weiterentwickelt, so dass es unterschiedliche Versionen der MFC gibt.
Ein Überblick über die Vererbungshierarchie befindet sich in der Hilfe unter
„Hierarchy Chart„.
Auf diese Klassen greift der Generator AppWizard zu, der ein erstes
Programmgerüst erstellt. Dieses Gerüst kann dann nach eigenen Wünschen
abgeändert werden. Hierbei verändert man aber nicht die Klassenbibliothek selbst.
Vielmehr werden neue, eigene und damit spezialisierte Klassen abgeleitet. Diese
eigenen Klassen „erben„ dabei die gesamte Funktionalität ihrer Vorgängerklasse(n).
Da die Vorgängerklassen bereits die wesentlichen Funktionen der WindowsOberfläche nutzen, müssen lediglich die „richtigen„ Objekte zusammenstellt werden,
um eine eigene Anwendung zu erstellen.
5.2
Schlüsselkonzepte
Die folgenden Punkte bilden die Schlüsselkonzepte eines Visual C++ Programms:
•
Das „Anwendungsobjekt„ bildet den Kern der Anwendung. Es existiert nur einmal.
Das
Anwendungsobjekt
verwaltet
eine
Liste
von
Dokumenten
(Dokumentobjekten). Es nimmt die Nachrichten von Windows entgegen und
verteilt sie an seine Objekte bzw. gibt Nachrichten (Botschaften) an andere
Objekte weiter.
OOM/OOP
147
•
Die Daten, mit denen diese Anwendung arbeitet, sind in einem
Dokumentenobjekt zusammengefasst. Dokumentenobjekte verwalten jeweils ein
einzelnes Dokument, indem sie hauptsächlich den Transfer der Daten von und
zum Massenspeicher organisieren.
•
Der Benutzer sieht auf dem Bildschirm ein „Ansichtsobjekt„ (ein WindowsFenster), über das er mit seiner Anwendung kommuniziert. Auf einem solchen
Fenster sind die Daten eines Dokumentes dargestellt. Oft reicht das
Ansichtsobjekt nicht aus, alle Daten eines Dokuments darzustellen
(beispielsweise bei Word). In diesem Fall zeigt das Ansichtsobjekt nur einen
Ausschnitt aus einem Dokument.
Das Ansichtsobjekt nimmt die Benutzeraktionen (Ereignisse) entgegen. Sie
werden durch Maus- oder Tastatureingaben erzeugt. Daneben gibt es eine Reihe
anderer Ereignisse. So kann eine Schnittstelle, der Zeitgeber usw. Ereignisse
auslösen. Diese Ereignisse aktivieren i. a. eine Ereignisprozedur.
Die wesentliche Aufgabe des
Ereignisprozeduren zu definieren.
•
5.3
Programmierers
besteht
darin,
diese
Das Ansichtsobjekt (Fenster) stellt die oberste Hierarchiestufe einer Anwendung
dar (Windows selbst kennt mehrere Anwendungen). Auf einem Fenster befinden
sich eine Reihe von Elementen wie Menüs, Symbolleisten, Schaltflächen, Listen,
Eingabefelder, Optionenfelder, Markierungskästchen usw. Ein Klick auf eines
dieser Elemente führt zu einer speziellen Botschaft an die Anwendung, die eine
Reaktion hervorruft.
Arbeiten mit der Programmierumgebung von Visual C++
Normalerweise wird ein Windows-Programm mit den Hilfsmitteln, die das Developer
Studio zur Verfügung stellt, erzeugt. D. h. man geht weg von der DOS-orientierten
Console Anwendung, um die vielfältigen Möglichkeiten von Windows zu nutzen.
Die wesentlichen Aufgaben des Programmierens bestehen dabei aus:
•
Definition einer oder
Anwendungsdaten.
•
Entwurf von Fenstern zur Darstellung der Anwendungsdaten und Festlegung der
interaktiven Elemente zur Verarbeitung der Daten.
•
Festlegung von Menüs, Symbolleisten, Schaltflächen und anderer interaktiver
Elemente sowie deren Verknüpfung mit Ereignisprozeduren.
mehrerer
Dokumentklassen
zur
Verwaltung
der
In der Praxis wird zwischen folgenden Schritten unterschieden:
1. Erstellen eines Programmgerüstes mit AppWizard. Dies ist ein einmaliger, nicht
korrigierbarer Schritt. Eine gewisse Sorgfalt ist daher angebracht, da Fehler nur
mit hohem Aufwand korrigiert werden können. Unter Umständen muss das ganze
Programm neu generiert werden.
148
OOM/OOP
Mit AppWizard wird hauptsächlich festgelegt, ob die Anwendung nur ein Fenster
(SDI Single Document Interface) oder mehrere Fenster (MDI Multiple Document
Interface) haben soll, ob eine Symbolleiste verwenden werden soll, ob das
Programm drucken soll und ob eine kontextsensitive Hilfe zur Verfügung stehen
soll.
Aufgrund dieser Einstellungen wird von AppWizard ein Programmgerüst
generiert, das eine Anwendungsklasse, eine Dokumentenklasse, eine
Ansichtsklasse und eine Klasse für das Hauptfenster der Anwendung besitzt. Bei
dieser Gelegenheit wird festgelegt, ob das Fenster Bildlaufleisten enthalten soll
oder nicht usw. Für diese sichtbaren Objekte werden bereits die
Ressourcendateien angelegt.
2. Mit AppStudio werden dann die Ressourcen bearbeitet und erweitert. Hierbei
handelt es sich um das Festlegen der Menüpunkte, Festlegen von Schnell- und
Kurztasten, Bearbeiten von Ikonen und Bitmaps und die Konstruktion ganzer
Benutzerdialoge (Dialogfenster).
3. Über den ClassWizard wird die Verbindung aller interaktiven Elemente zum
Programm hergestellt. Hierbei wird definiert, welche Ereignisse überhaupt
berücksichtigt und wie die Daten mit den Dialogfenstern ausgetauscht werden.
Der ClassWizard ergänzt das Programmgerüst automatisch durch vorgefertigte
Prozeduren für jedes Ereignis und legt Klassenvariablen an.
4. Mit dem Editor und dem Browser der Visual Workbench wird nun die
Verarbeitungslogik ergänzt. Hierbei kann Ereignis für Ereignis einzeln
abgearbeitet werden, wobei die Unabhängigkeit der Ereignisse ein wesentliches
Entwurfsziel sein sollte. Schließlich kann der Benutzer mit der Maus alles
anklicken.
5. Compiler und Linker versuchen nun ein lauffähiges Programm zu erzeugen.
Fehler werden mit dem Editor beseitigt.
6. Eine wesentliche Hilfe bei der Fehlersuche stellt der Debugger dar, der den
Programmablauf zeigt, Daten anzeigt, diese gegebenenfalls korrigieren lässt usw.
5.4
Programmvorbereitung
Die dargestellten Schritte erfordern ein Mindestmaß an Planung:
1. Festlegung der Datenstrukturen von Dokumenten (Was soll eigentlich dargestellt
werden?).
2. Serialisierung der Daten eines Dokuments (Wie sollen die Daten über das
Programmende hinaus gespeichert werden?). Hierbei kann es sich um das
Speichern eines beliebigen Fensterinhaltes (z. B. einer Zeichnung), eines genau
festgelegten Datensatzes und/ oder ganzer Objekte handeln.
3. Darstellung der Daten über das Ansichtsobjekt (Wie soll das Fenster aufgeteilt
sein? Reicht ein Fenster?).
OOM/OOP
149
4. Reaktion auf Tastatur- und Mausereignisse (Auf welche Ereignisse reagiert das
Programm wie?).
5. Reaktion auf Menü- und Ikonenereignisse (Welche Punkte sind enthalten?
Welche Ikonen sollen dargestellt werden?).
6. Erweiterungen des vom AppWizard Programmgerüstes (geteilte Fenster usw.).
7. Erstellen der Druckausgabe.
8. Aufbau des Hilfesystems.
9. Erzeugen eines Setup-Programms zur Anwendungsweitergabe.
5.5
Programmgerüst
Es wurden bereits mehrfach die Klassen erwähnt, die mehr oder weniger
automatisch angelegt werden. Es handelt sich hierbei um Nachkommen von
Basisklassen der MFC. Die wesentlichen Klassen von Programmen werden im
folgenden noch einmal etwas ausführlicher behandelt (Abbildung 6.1).
Abbildung 6.1: Ableitung der generierten Klassen aus der MFC
•
CObject
ist die Basis fast aller anderen Klassen der Bibliothek. Diese Klasse ermöglicht unter
anderem das zwischenzeitliche „Abladen„ (Serialisieren) von Objekten auf
Speichermedien wie der Festplatte und die Abfrage von Informationen über die
Klasse zur Laufzeit eines Programms.
•
CCmdTarget
stellt die Basis für die Empfängerlisten-Architektur der Bibliotheksklassen dar. Eine
Empfängerliste ordnet den Kommandos und Nachrichten die Methoden zu, die zu
deren Bearbeitung bereitgestellt wurden. Ein Kommando ist eine Nachricht von
einem Menüpunkt, einer Schaltfläche oder einer Schnelltaste.
150
•
OOM/OOP
CWinApp
stellt die Basis für Anwendungsobjekte dar, repräsentiert also ein Programm unter
Microsoft Windows. Die von CWinApp definierten Methoden ermöglichen sowohl die
Initialisierung einer Anwendung (und jeder weiteren Kopie) als auch den eigentlichen
Start. Jede Anwendung, die die MFC-Klassen verwendet, kann nur ein CWinAppObjekt enthalten. Dieses Objekt wird gemeinsam mit den anderen globalen C++Objekten erstellt und ist bereits verfügbar, wenn Windows die (ebenfalls von der
Bibliothek definierte) Funktion WinMain aufruft. Deshalb muss das CWinApp-Objekt
auf Dateiebene deklariert sein.
•
CDocTemplate
ist eine abstrakte Basisklasse und stellt die grundlegende Funktionalität für
Dokumentenvorlagen bereit. Eine solche Vorlage definiert die Beziehung zwischen
drei Arten von Klassen:
-
Eine von CDocument abgeleitete Dokumentenklasse.
-
Eine Ansichtsklasse, die Daten der Dokumentenklasse darstellt. Diese Klasse
lässt sich von CView, CScrollView, CFormView oder CEditView ableiten
(CEditView ist auch direkt verwendbar).
-
Eine Rahmenfensterklasse für das Dokument, die die Ansicht enthält. Für eine
SDI-Anwendung wird diese Klasse von CFrameWnd abgeleitet, für MDIAnwendungen dagegen von CMDIChildWnd. Falls keine programmspezifischen
Anpassungen des Rahmenfensters notwendig sind, lässt sich die Klasse
CFrameWnd bzw. CMDIChildWnd auch direkt verwenden.
•
CWnd
stellt die grundlegende Funktionalität aller Fensterklassen der MFC zur Verfügung.
Ein Objekt der Klasse CWnd ist nicht dasselbe wie ein Fenster unter Windows,
obwohl beide eng miteinander verknüpft sind: CWnd-Objekte werden über den
Konstruktor ihrer Klasse erzeugt und über den Destruktor wieder abgebaut, d. h. sie
sind programmeigene Datenstrukturen. Ein Fenster von Windows stellt dagegen eine
interne Datenstruktur des Betriebssystems dar, die mit Hilfe der Methode Create
erzeugt bzw. über den virtuellen Destruktor von CWnd und die Windows-Funktion
DestroyWindow wieder freigegeben wird.
Die Klasse CWnd und die mit ihr aufgebauten Mechanismen zur Weitergabe von
Botschaften verkapseln die Funktion WndProc, die sich in traditionellen WindowsAnwendungen findet: Botschaften werden hier anhand einer Empfängerliste zu der
entsprechenden OnMessage-Methode der Klasse CWnd weitergeleitet. Zur
Behandlung einer bestimmten Botschaft kann eine eigene Klasse von CWnd
abgeleitet und in die Empfängerliste eine entsprechende OnMessage-Methode
eintragen werden bzw. die Standardvariante dieser Methode ersetzt werden.
OOM/OOP
151
Die Klasse CWnd stellt nicht nur die Basis für das Hauptfenster einer Anwendung,
sondern auch für Child-Fenster dar. In beiden Fällen wird eine eigene Klasse von
CWnd abgeleitet und ihr werden die gewünschten programmspezifischen Datenfelder
hinzugefügt. Danach brauchen in dieser Klasse nur noch die erforderlichen
Methoden zur Behandlung von Botschaften und Kommandos implementiert und eine
Empfängerliste hinzugefügt werden, die einzelne Botschaftstypen mit diesen
Routinen verbindet.
Das Erzeugen von Child-Fenstern erfolgt in zwei Schritten: Nach dem Aufruf des
Konstruktors zum Anlegen des Objekts wird über die Methode Create ein WindowsFenster erstellt und mit dem CWnd-Objekt verbunden. Falls der Benutzer das
Fenster schließt, muss entweder die Methode DestroyWindow zum Abbau des
Windows-Fensters oder der Destruktor für das Objekt aufgerufen werden.
•
CFrameWnd
bietet die volle Funktionalität eines überlagerten Windows-SDI-Dokumentenfensters
bzw. Popup-Dokumentenfensters einschließlich der Komponenten zur Verwaltung.
Für Dokumentenfenster eigener Anwendungen wird eine von CFrameWnd
abgeleitete Klasse um anwendungsspezifische Datenfelder ergänzt. Die Bearbeitung
von Botschaften durch Objekte abgeleiteter Klassen geschieht über die Definition
und Zuordnung entsprechender Methoden.
Insgesamt gibt es drei
Dokumentenfensters:
verschiedene
Möglichkeiten
-
Die direkte Konstruktion unter Verwendung von Create.
-
Die direkte Konstruktion mit Hilfe von LoadFrame.
-
Die indirekte Konstruktion mit einer Dokumentenvorlage.
•
CView
zur
Erstellung
eines
stellt die grundlegende Funktionalität für benutzerdefinierte Ansichten bereit. Eine
Ansicht ist mit einem Dokument verbunden und agiert als eine Art Vermittler
zwischen dem Anwender und dem Dokument: Sie stellt einen Ausschnitt des
Dokuments auf dem Bildschirm oder Drucker dar und interpretiert
Anwendereingaben als Aktionen damit.
Ansichten sind grundsätzlich einem Rahmenfenster untergeordnet, wobei sich unter
Umständen mehrere Ansichten ein und dasselbe Fenster teilen (vgl.
CSplitterWnd). Die Beziehung zwischen einer Ansicht, einem Dokument und
einem Rahmenfenster wird durch ein CDocTemplate-Objekt festgelegt. Wenn der
Anwender ein neues Fenster öffnet oder ein existierendes Fenster teilt, erstellt das
Programmgerüst eine neue Ansicht und verbindet sie mit dem Dokument.
Eine Ansicht kann zu jedem Zeitpunkt nur mit einem Dokument verbunden sein, ein
Dokument aber sehr wohl mehrere Ansichten haben, die entweder in einem
152
OOM/OOP
gemeinsamen (geteilten) Rahmenfenster oder in separaten Rahmenfenstern
dargestellt werden. Dokumente lassen sich parallel mit verschiedenen Typen von
Ansichten verbinden: Eine Textverarbeitung könnte beispielsweise den zu
bearbeitenden Text einmal in normaler Darstellung und über eine zweite Ansicht als
Gliederung zeigen. Ansichten verschiedener Typen lassen sich entweder in
separaten Rahmenfenstern oder über ein gemeinsames statisch geteiltes Fenster
darstellen.
Ansichtsobjekte sind für die Interaktion mit dem Benutzer zuständig, d. h. sie
empfangen vom Rahmenfenster weitergereichte Kommandos sowie Botschaften
über Tastatur- und Mausereignisse. Nicht bearbeitete Botschaften werden an das
Rahmenfenster zurückgegeben, das sie gegebenenfalls an das Anwendungsobjekt
weiterreicht. Wie alle Befehlsempfänger verwenden auch Ansichten eine
Empfängerliste
zur
Zuordnung
von
Botschaften,
Kommandos
und
Behandlungsroutinen.
Eine Ansicht ist für die Darstellung und Veränderung des Dokuments, nicht aber für
die Speicherung dieser Daten zuständig. Man kann eine Ansicht entweder direkt auf
die Datenstrukturen des Dokuments zugreifen lassen oder – die ist meist
empfehlenswerter – in der Dokumentenklasse entsprechende Zugriffs- und
Abfragemethoden definieren.
Wenn sich die Daten eines Dokuments ändern, muss die für die Änderungen
verantwortliche Ansicht die Methode CDocument::UpdateAllViews aufrufen, die
dann sämtliche anderen Ansichten des Dokuments über die Methode
CView::OnUpdate benachrichtigt. Die Standardvariante von OnUpdate
kennzeichnet den gesamten Anwendungsbereich der Ansicht als ungültig und sollte
nach Möglichkeit durch eine eigene Version ersetzt werden, die sich bei
Neuausgaben auf die echten Veränderungen beschränkt.
CView ist eine abstrakte Basisklasse und macht deshalb in jedem Fall eine eigene
Ableitung nötig, die minimal eine eigene Version der Methode OnDraw zum Zeichnen
der Daten des Dokuments definieren muss. Über OnDraw werden nicht nur
Ausgaben auf den Bildschirm, sondern auch die Druckvorschau und die
Druckausgabe selbst realisiert.
Botschaften von Bildlaufleisten bearbeitet eine Ansicht über die Methoden
OnHScroll und OnVScroll, deren Standardvarianten leere Funktionsrümpfe sind.
Diese beiden Methoden können durch eigene Versionen ersetzt werden – oder es
kann gleich die von CView abgeleitete Klasse CScrollView verwendet werden, die
diese Aufgaben automatisch übernimmt.
•
CDialog
ist die Basisklasse für die Darstellung von Dialogfeldern auf dem Bildschirm und
implementiert sowohl modale als auch nichtmodale Dialogfelder. Ein modales
Dialogfeld muss vom Benutzer geschlossen werden, bevor er die Arbeit mit der
Anwendung fortsetzen kann, ein nichtmodales Dialogfeld erlaubt dagegen weitere
OOM/OOP
153
Operationen auch während des Zeitraums, in dem sich das Dialogfeld auf dem
Bildschirm befindet.
Ein CDialog-Objekt stellt eine Kombination aus einer Dialogschablone bzw. –
Ressource und einer von CDialog abgeleiteten Klasse dar. Die Dialogschablone
lässt sich mit Hilfe von AppStudio erstellen und in einer Ressource speichern. Mit
ClassWizard kann man dann eine von CDialog abgeleitete Klasse erstellen.
Ein Dialogfeld empfängt wie jedes andere Fenster Botschaften von Windows. In
einem Dialogfeld interessiert vorrangig die Behandlung von Statusnachrichten von
Kontrollelementen, da dies die Interaktionen des Anwenders mit dem Dialogfeld
widerspiegelt. ClassWizard listet für jedes Kontrollelement im Dialogfeld die
möglichen Statusnachrichten auf, wobei ausgewählt werden kann, welche dieser
Nachrichten das Programm bearbeiten soll. ClassWizard fügt dann die
entsprechenden Empfängerlisteneinträge und Bearbeitungsroutinen zur neuen
Klasse hinzu; das Ausfüllen dieser Funktionsrümpfe ist wiederum Sache des
Programmierers.
•
CDocument
stellt die grundlegende Funktionalität für benutzerdefinierte Dokumentenklassen
bereit. Dokumentenklassen repräsentieren Dokumente – also Datensammlungen, die
der Benutzer normalerweise en bloc über Menübefehle wie Datei öffnen in den
Hauptspeicher lädt bzw. mit Datei speichern als Datei schreibt.
CDocument unterstützt die Standardoperationen wie das Erstellen, Laden und
Speichern eines Dokuments. Das Programmgerüst bearbeitet Dokumente über die
durch CDocument definierte Schnittstelle.
Eine Anwendung kann mehrere Dokumententypen unterstützen (z. B. Rechenblätter
und Textdokumente). Jedem Dokumententyp ist eine Dokumentenvorlage
zugeordnet: Der Programmentwickler legt fest, welche Ressourcen (z. B. Menüs,
Symbole und Schnelltasten) zu einem Dokument gehören. Jedes Dokument enthält
einen Zeiger auf das ihm zugeordnete CDocTemplate-Objekt.
Anwender interagieren mit einem Dokument über ein oder mehrere mit ihm
verbundene Ansichten, die Objekte der Klasse CView darstellen. Eine Ansicht
umgibt das Abbild des Dokuments in einem Dokumentenfenster und interpretiert die
Eingaben des Anwenders als Aktionen mit den Daten des Dokuments. Ein Dokument
kann mehrere mit ihm verbundene Ansichten haben. Wenn der Anwender ein
Fenster für ein Dokument öffnet, erstellt das Programmgerüst eine Ansicht und
verbindet sie mit dem Dokumentenobjekt. Die Dokumentenvorlage legt dabei den
Typ der Ansicht und des Dokumentenfensters fest.
Dokumente werden in die vom Programmgerüst definierte Nachrichtenkette mit
einbezogen und stellen deshalb Befehlsempfänger für Kommandos der
Benutzeroberfläche dar. Von einem Dokumentenobjekt nicht bearbeitete
Kommandos werden an die Dokumentenvorlage weitergegeben, die ihrerseits
154
OOM/OOP
entweder die Bearbeitung übernimmt oder das Kommando an weitere Objekte des
Programms weiterreicht.
Wenn die Daten eines Dokuments verändert wurden, muss jede seiner Ansichten
diese Veränderung widerspiegeln. CDocument definiert eine Methode namens
UpdateAllViews, die sämtliche mit einem Dokument verbundenen Ansichten der
Reihe nach zum Neuzeichnen ihres Fensters auffordert. Außerdem kann ein
Dokumentenobjekt beim Schließen von Ansichten gegebenenfalls dafür sorgen, dass
der Benutzer eine Rückfrage des Programmgerüsts und die Gelegenheit zum
Speichern veränderter Daten erhält.
5.6
Ableiten eigener Klassen von der MFC
5.6.1 Grundlagen
Normalerweise werden bereits mit der Generierung unserer Oberfläche eine Vielzahl
von eigenen Klassen aus der MFC abgeleitet. Dies erkennt man an den
entsprechenden class Anweisungen, die sich in den generierten Units (genauer in
deren Kopfdateien) befinden.
class CTestDoc : public CDocument
Mit dieser Anweisung wird eine neue Klasse CTestDoc angelegt, die sich von
CDocument ableitet. Dies bedeutet nichts anderes, als dass unsere eigene Klasse
alle Daten und alle Methoden erbt.
Es können grob gesprochen folgende Bereiche in der MFC festgestellt werden, von
denen solche eigenen Ableitungen durchgeführt werden können:
• Rahmenfenster
• Dokumente
• Dokumentansichten
• Mehrere Ansichten
• Spezielle Ansichtstypen, wie z.B. Bildlauf- und Formularansichten
• Dialogfelder und Eigenschaftenfenster
• Windows-Standardsteuerelemente
• Zuordnen von Windows-Nachrichten zu Behandlungsroutinen
• Symbolleisten und andere Steuerleisten
• Drucken und Druckvorschau
• Serialisierung von Daten in/aus Dateien und anderen Medien
• Gerätekontexte und GDI-Zeichenobjekte
• Ausnahmebehandlung
• Auflistungen von Datenobjekten
OOM/OOP
155
• Diagnose
• Zeichenfolgen, Rechtecke und Punkte
• Datum und Zeit
Bei eine Windows Anwendung bearbeitet der Benutzer Dokumente (Microsofts
Bezeichnung für eine Menge beliebiger Daten), die in einem Rahmenfenster
dargestellt werden. Ein solches Rahmenfenster mit Dokument enthält somit die
beiden Hauptkomponenten Rahmen und Dokument. Der Bereich des Fensters ohne
Rahmen, Titel-, Menü-, Symboleiste usw. wird Client-Bereich (Innenbereich)
genannt. Dieser Bereich selbst kann wiederum von einem einzigen Dokument (SDI =
Single Document Interface Anwendung) oder von mehreren Dokumenten (MDI
=Multiple Document Interface Anwendung) belegt sein.
In den meisten Anwendungen werden die Sicht(en) auf ein oder mehrere Dokumente
von den eigentlichen Dokumenten getrennt. Eine normale Anwendung sieht daher
wie in Abbildung 6.2 aus.
Abbildung 6.2: Trennung von Ansicht und Dokument
Es wird ein Rahmenfensterobjekt angelegt, in dessen Innenbereich Ansichten
dargestellt werden. Der äußere Rahmen sowie die Darstellung im Innenbereich in
Ansichten wird durch zwei unterschiedliche Klassen in der MFC verwaltet. Dabei
stellt das Ansichtsfenster ein untergeordnetes Rahmenfenster dar, das sich
weitgehend wie das äußere Rahmenfenster verhält, selbst aber keine weiteren
Ansichten verwaltet.
Eine MDI Anwendung muss nicht unbedingt auf eine einzige Ansicht beschränkt
bleiben. So können durchaus mehrere Ansichten auf das gleiche Dokument kreiert
werden, wobei der Dokumentinhalt immer konsistent bleibt. Die Ansichtsobjekte
kommunizieren beide mit demselben Dokument.
Es ist aber auch denkbar, mehrere Dokumente gleichzeitig zu öffnen. Hierbei ist es
nur natürlich mindestens eine Sicht pro Dokument anzulegen. Aber auch die
Kombination mehrerer Sichten und mehrerer Dokumente sind denkbar.
156
OOM/OOP
5.6.2 Rahmenfenster und unterteilte Fenster (Splitterbox)
Ab Windows 95 wechselt der Trend vom MDI Konzept der Version 3.1 mit mehreren
Fenstern hin zu Rahmenfenstern mit Scheiben (Butzenscheiben). Der Benutzer kann
die Stege zwischen den Scheiben bewegen. Damit verändert er aber die Darstellung
mehrerer Scheiben (Abbildung 6.3).
Abbildung 6.3: Ein Dokument mit mehreren Ansichten
Dabei ist es auch wie dargestellt denkbar, die Ansichten in ihrer Symbolik zu
verändern, also z. B. Datenreihen als Diagramme, Dateien und Verzeichnisbaum
usw. anzuzeigen. Dabei werden aus demselben Dokumenttyp mehrere
unterschiedliche Ansichten generiert. So erzeugt der Windows Explorer aus der
Dateistruktur einmal den Verzeichnisbaum, zum anderen aber auch die Dateiliste.
5.6.3 Mehrere Dokumenttypen, Ansichten und Rahmenfenster
Um die letzten Aussagen noch einmal zusammenzufassen, lässt sich feststellen,
dass die meisten Anwendungen nur einen oder doch zumindest ähnliche
Dokumenttypen unterstützen. Bei einem Grafikprogramm kann man durchaus
diskutieren, ob der Dokumenttyp nun „Bild„ oder „BMP-Bild„, „PCX-Bild„ usw. ist.
Eine reine Palette wäre dagegen deutlicher abzugrenzen.
Die einfachste Anwendung kann nun einen Dokumenttyp in Form eines konkreten
Dokuments in einer einzigen Ansicht in einem Rahmenfenster darstellen.
OOM/OOP
157
Als erste Steigerung können folgende Fälle angesehen werden:
− mehrere, gleichzeitig geöffnete Dokumente mit je einer Ansicht
− ein geöffnetes Dokument mit mehreren Ansichten.
Der allgemeinste Fall besteht in mehreren Dokumenten, die wiederum jedes für sich
in mehreren Ansichten auftreten kann.
Durch das Generieren einer einzigen Dokumentklasse vom Anwendungsassistenten,
legt man sich auf einen einzigen Dokumenttyp fest. Dieser Dokumenttyp hat auch
nur eine Dokumentvorlage.
Mit dem Ableiten einer MDI Anwendung eröffnet sich die Möglichkeit, verschiedene
Dokumenttypen zu verarbeiten.
Hierzu aktiviert man normalerweise den Klassenassistenten und generiert eine neue
Klasse vom Typ CDocument pro gewünschter Klasse. Diese Klasse muss nun mit
den speziellen Datendeklarationen gefüllt werden, die unsere Anwendung benötigt.
Um bei der Neuanlage eines solchen Dokuments durch den Benutzer vorgeben zu
können, muss noch eine Vorlage pro Dokumentklasse generiert werden. Dies
geschieht durch den Aufruf von AddDocTemplate in der Methode InitInstance
unserer Anwendung.
Der Einsatz von mehreren Dokumentklassen oder mehreren Sichten auf dasselbe
Dokument wird intern durch Listen verwaltet. So besitzt jedes Dokument eine Liste
(genauer einen Zeiger auf eine Liste) aller seiner Sichten. Wird eine Ansicht gelöscht
oder eine solche neu geöffnet, dann muss die Liste verlängert oder gekürzt werden.
Ändert sich der Inhalt eines solchen Dokuments, so muss mit UpdateAllViews
eine Schleife aktiviert werden, die alle Sichten des Dokuments neu zur Anzeige
bringt.
MFC unterstützt drei allgemeine Benutzeroberflächen, die mehrere Ansichten für
dasselbe Dokument benötigen. Diese Modelle sind (Abbildung 6.3):
•
Ansichtsobjekte derselben
Dokumentrahmenfenster.
•
Ansichtsobjekte derselben Klasse in demselben Dokumentrahmenfenster.
Klasse,
jedes
in
einem
eigenen
• Ansichtsobjekte verschiedener Klassen in einem einzelnen Rahmenfenster.
MDI-
158
6 Anhang
OOM/OOP
OOM/OOP
6.1
159
Beispiel „Stuetzenprogramm“
Als Beispiel wird das in der C-Vorlesung vorgestellte Stuetzenprogramm hier als
objektorientiertes Programm vorgestellt. Erläuterungen zu den einzelnen
Stützentypen und der Funktionalität des Beispiels sind im C-Beispiel enthalten.
Das
hier
vorgestellte
Beispiel
liegt
im
CIP-Pool
im
"P:\Bauinf\OOStuetzen\OOStuetzenconsole" zum downloaden bereit.
Verzeichnis
Objektmodell:
CStuetzenProg
{1}
1
cRechtStuetze
0..100
cStuetze
{n}
cEllipStuetze
cAchteStuetze
cKreisStuetze
160
OOM/OOP
6.1.1 Klasse cStuetzenProg (StuetzenProg.h)
// Klasse cStuetzenProg
#ifndef STUETZENPROG_H
#define STUETZENPROG_H
#include "Global.h"
class cStuetzenProg
{
private:
cStuetze *m_pFeldStuetze[MAX_N];
//Feld mit Zeigern auf Stuetzen
int m_nAnzahl;
void Eingabe(void);
void Berechnen(void);
void Ausgabe(void);
int GetTypID(wort w);
public:
// Konstruktor und Destruktor
cStuetzenProg::cStuetzenProg(void);
cStuetzenProg::~cStuetzenProg(void);
void Run(void);
//void printf(char *p){};
//Achtung use scope resolution operator
//to reach printf from stdio.h
};
#endif //#ifndef
6.1.2 Klasse cStuetzenProg (StuetzenProg.cpp)
// Klasse cStuetzenProg
#include <stdio.h>
#include <string.h>
#include "Global.h"
#include "Stuetze.h"
#include "StuetzenProg.h"
// Konstruktor und Destruktor
cStuetzenProg::cStuetzenProg(void)
{
}
OOM/OOP
161
cStuetzenProg::~cStuetzenProg(void)
{
for (int i=0; i<m_nAnzahl; i++)
delete m_pFeldStuetze[i];
}
void cStuetzenProg::Run(void)
{
Eingabe();
Berechnen();
Ausgabe();
}
void cStuetzenProg::Berechnen(void)
{
int i;
for (i=0;i<m_nAnzahl;i++)
m_pFeldStuetze[i]->Berechnen();
}
void cStuetzenProg::Eingabe(void)
{
int i,j,flag;
wort tmpwort;
// Eingabe der Anzahl der Stützen
do
{
flag = 1;
printf("\nWieviele Stuetzen sollen berechnet
werden? : ");
scanf("%d",&m_nAnzahl);
// Eingabe prüfen
if ((m_nAnzahl < 0) || (m_nAnzahl > MAX_N))
{
printf("\nUngueltige Eingabe !");
flag = 0;
}
} while (flag == 0);
i = 0;
do
{
printf("\nKennwort eingeben : ");
scanf("%s", tmpwort);
// Eingabe eines Rechtecks
j = GetTypID(tmpwort);
switch (j) {
162
OOM/OOP
case 1: m_pFeldStuetze[i] = new cRechtStuetze;
break;
case 2: m_pFeldStuetze[i] = new cEllipStuetze;
break;
case 3: m_pFeldStuetze[i] = new cAchteStuetze;
break;
case 4: m_pFeldStuetze[i] = new cKreisStuetze;
break;
default: printf("\nUngueltige Eingabe, bitte
nochmal eingeben !\n");
continue;
}
m_pFeldStuetze[i]->Eingabe();
i++;
} while (i < m_nAnzahl);
}
int cStuetzenProg::GetTypID(wort w)
{
if (strcmp(w,"recht") == 0)
return (1);
// Eingabe eines Ellipse
else if (strcmp(w,"ellip") == 0)
return (2);
// Eingabe eines Achtecks
else if (strcmp(w,"achte") == 0)
return (3);
// Eingabe eines Kreis
else if (strcmp(w,"kreis") == 0)
return (4);
else
return (-1);
}
void cStuetzenProg::Ausgabe(void)
{
int i;
//Alle Rechteckstuetzen
for (i=0;i<m_nAnzahl;i++)
if (GetTypID(m_pFeldStuetze[i]->GetKennwort())==1)
m_pFeldStuetze[i]->Ausgabe();
for (i=0;i<m_nAnzahl;i++)
if (GetTypID(m_pFeldStuetze[i]->GetKennwort())==2)
m_pFeldStuetze[i]->Ausgabe();
for (i=0;i<m_nAnzahl;i++)
if (GetTypID(m_pFeldStuetze[i]->GetKennwort())==3)
OOM/OOP
163
m_pFeldStuetze[i]->Ausgabe();
for (i=0;i<m_nAnzahl;i++)
if (GetTypID(m_pFeldStuetze[i]->GetKennwort())==4)
m_pFeldStuetze[i]->Ausgabe();
}
7
7.1.1 Klasse cStuetze (Stuetze.h)
// Klasse cStuetze
#ifndef STUETZE_H
#define STUETZE_H
class cStuetze
{
protected:
wort
double
double
double
m_Kennw;
m_dFlaeche;
m_dVolumen;
m_dHoehe;
//
//
//
//
Kennwort, Stuetzentyp
Ergebniss
Ergebniss
Hoehe der Stütze
void
SetHoehe(double h);
double GetHoehe(void);
public:
// Konstruktor und Destruktor
cStuetze(void);
~cStuetze(void);
// Zugriff auf private Member
char *GetKennwort(void){return m_Kennw;};
// Virtuelle Funktionen
virtual void Eingabe(void) {};
virtual void Berechnen(void) {};
virtual void Ausgabe(void) {};
};
class cRechtStuetze : public cStuetze
{
private :
double m_dBreite; // Breite der Stütze
double m_dLaenge; // Länge der Stütze
public:
cRechtStuetze(void);
~cRechtStuetze(void);
// Virtuelle Funktionen
virtual void Eingabe(void);
164
OOM/OOP
virtual void Berechnen(void);
virtual void Ausgabe(void);
};
class cEllipStuetze : public cStuetze
{
private :
double m_dSeiteA; // Seite1 der Stütze
double m_dSeiteB; // Seite2 der Stütze
public:
cEllipStuetze(void);
~cEllipStuetze(void);
// Virtuelle Funktionen
virtual void Eingabe(void);
virtual void Berechnen(void);
virtual void Ausgabe(void);
};
class cAchteStuetze : public cStuetze
{
private :
double m_dSeiteA; // Kantenlänge der Stütze
double m_dSeiteB; // Breite der Stütze
public:
cAchteStuetze(void);
~cAchteStuetze(void);
// Virtuelle Funktionen
virtual void Eingabe(void);
virtual void Berechnen(void);
virtual void Ausgabe(void);
};
class cKreisStuetze : public cStuetze
{
private :
double m_dDurch1; // Durchmesser D1 der Stütze
double m_dDurch2; // Durchmesser D2 der Stütze
public:
cKreisStuetze(void);
~cKreisStuetze(void);
// Virtuelle Funktionen
virtual void Eingabe(void);
virtual void Berechnen(void);
virtual void Ausgabe(void);
};
#endif //ifndef
OOM/OOP
165
7.1.2 Klasse cStuetze (Stuetze.cpp)
// Klasse cStuetze und Unterklassen
#include
#include
#include
#include
#include
<stdio.h>
<string.h>
"Global.h"
"Stuetze.h"
"StuetzenProg.h"
// Konstruktor und Destruktor
cStuetze::cStuetze(void)
{
m_dFlaeche = 0;
m_dVolumen = 0;
m_dHoehe
= 0;
}
cStuetze::~cStuetze(void)
{
}
void cStuetze::SetHoehe(double h)
{
m_dHoehe = h;
}
double cStuetze::GetHoehe(void)
{
return m_dHoehe;
}
// *************************
// Klasse cRechtStuetze
// *************************
cRechtStuetze::cRechtStuetze(void)
{
strcpy(m_Kennw, "recht");
}
cRechtStuetze::~cRechtStuetze(void)
{
}
void cRechtStuetze::Eingabe(void)
{
double r;
166
OOM/OOP
printf("\nSie wollen ein Rechteck eingeben !");
printf("\nBitte geben Sie a, b, h ein: ");
scanf("%lf%lf%lf", &m_dBreite, &m_dLaenge, &r);
SetHoehe(r);
}
void cRechtStuetze::Berechnen(void)
{
m_dFlaeche = m_dBreite * m_dLaenge;
m_dVolumen = m_dFlaeche * m_dHoehe;
}
void cRechtStuetze::Ausgabe(void)
{
printf("\nRechteck : a= %.2lf,b = %.2lf,h = %.2lf,F =
%.2lf,V = %.2lf",
m_dBreite,m_dLaenge,GetHoehe(),m_dFlaeche,m_dVolumen);
}
// *************************
// Klasse cEllipStuetze
// *************************
cEllipStuetze::cEllipStuetze(void)
{
strcpy(m_Kennw, "ellip");
}
cEllipStuetze::~cEllipStuetze(void)
{
}
void cEllipStuetze::Eingabe(void)
{
double r;
printf("\nSie wollen eine Ellipse eingeben !");
printf("\nBitte geben Sie a, b, h ein: ");
scanf("%lf%lf%lf", &m_dSeiteA, &m_dSeiteB, &r);
SetHoehe(r);
}
void cEllipStuetze::Berechnen(void)
{
m_dFlaeche = PI * m_dSeiteA * m_dSeiteB;
m_dVolumen = m_dFlaeche * GetHoehe();
}
void cEllipStuetze::Ausgabe(void)
{
OOM/OOP
167
printf("\nEllipse : a= %.2lf, b = %.2lf, h = %.2lf, F
= %.2lf, V = %.2lf", m_dSeiteA, m_dSeiteB, GetHoehe(),
m_dFlaeche, m_dVolumen);
}
// *************************
// Klasse cAchteStuetze
// *************************
cAchteStuetze::cAchteStuetze(void)
{
strcpy(m_Kennw, "achte");
}
cAchteStuetze::~cAchteStuetze(void)
{
}
void cAchteStuetze::Eingabe(void)
{
double r;
printf("\nSie wollen ein Achteck eingeben !");
printf("\nBitte geben Sie a, s, h ein: ");
scanf("%lf%lf%lf", &m_dSeiteA, &m_dSeiteB, &r);
SetHoehe(r);
}
void cAchteStuetze::Berechnen(void)
{
m_dFlaeche = 2 * m_dSeiteA * m_dSeiteB;
m_dVolumen = m_dFlaeche * GetHoehe();
}
void cAchteStuetze::Ausgabe(void)
{
printf("\nAchteck : a= %.2lf, b = %.2lf, h = %.2lf, F
= %.2lf, V = %.2lf", m_dSeiteA, m_dSeiteB, GetHoehe(),
m_dFlaeche, m_dVolumen);
}
// *************************
// Klasse cKreisStuetze
// *************************
cKreisStuetze::cKreisStuetze(void)
{
strcpy(m_Kennw, "kreis");
}
168
OOM/OOP
cKreisStuetze::~cKreisStuetze(void)
{
}
void cKreisStuetze::Eingabe(void)
{
double r;
printf("\nSie wollen ein Kreis eingeben !");
printf("\nBitte geben Sie D, d, h ein: ");
scanf("%lf%lf%lf", &m_dDurch1, &m_dDurch2, &r);
SetHoehe(r);
}
void cKreisStuetze::Berechnen(void)
{
m_dFlaeche = PI * ((m_dDurch1*m_dDurch1) (m_dDurch2*m_dDurch2)) / 4;
m_dVolumen = m_dFlaeche * GetHoehe();
}
void cKreisStuetze::Ausgabe(void)
{
printf("\nKreis : D= %.2lf,d = %.2lf,h = %.2lf,F =
%.2lf,V = %.2lf",
m_dDurch1,m_dDurch2,GetHoehe(),m_dFlaeche,m_dVolumen);
}
7.1.3 Globale Variablen (Global.h)
//Datei: Global.h
//Globale Vereinbarungen, Typen, Konstanten bzw. Makros
#define
#define
MAX_N
PI
100
3.1415927
typedef char wort[10];
7.1.4 Hauptprogramm (BspMain.cpp)
OOM/OOP
#include
#include
#include
#include
#include
169
<stdio.h>
<conio.h>
"Global.h"
"Stuetze.h"
"StuetzenProg.h"
void main(void)
{
cStuetzenProg myBsp;
myBsp.Run();
_getch(); //warte auf Tastendruck
}
170
OOM/OOP
OOM/OOP
171
8 Windowsprogrammierung (API)
8.1
Einleitung
•
Windows-Applikationen (Windows-Anwendungen) sind Programme, die eine
grafische Oberfläche (User Interface) mit Fenstertechnik zum Kommunizieren mit
dem Anwender benutzen.
•
Die von Microsoft eingeführte Programmierschnittstelle zu Windows API
(Application Programmers Interface) enthält die Grundfunktionen zum
Programmieren von Windows-Anwendungen.
•
Bei der klassischen Windows-Programmierung wird direkt die reine Schnittstelle
zur Anwendungsentwicklung (API) benutzt, anstatt irgendwelcher Verpackungen,
die das API unter einfacheren Schnittstellen verpacken.
Benutzersicht:
•
Benutzung des Programms mit Hilfe einer grafischen Oberfläche
•
Gleiches Erscheinungsbild verschiedener Programme
•
Gleichzeitige Verwendung verschienener Programme (Multitasking)
•
Großer Hauptspeicher für alle Programme (32 Bit virtueller Speicher)
Geschichtliche Entwicklung:
•
•
•
•
•
•
•
•
8.2
Grundlagen der grafischen Fensteroberfläche:
Mitte der 70er Jahre durch XEROX (PARC)
Einsatz durch Apple für den Macintosh 83
MS-Windows 1.01 im Jahr 85
MS-Windows 2.00 im Jahr 87
MS-Windows 3.00 im Jahr 90 (16 Mbyte)
MS-Windows 3.01 im Jahr 92 (OLE, Standard Dialoge)
MS-Windows 95 im Jahr 95
MS-Windows 98 im Jahr 98
Elemente einer Windows-Anwendung
Windows-Programme zeigen an der Oberfläche einen typischen Aufbau, bedingt
durch die von allen Programmen genutzten Darstellungselemente, die Windows zur
Verfügung stellt. Zwar muss der Programmierer einer Windows-Anwendung recht
viel Aufwand betreiben, um ein wirklich funktionales Programm zu erhalten,
172
OOM/OOP
allerdings wird er durch die Mechanismen von Windows und die zur Verfügung
stehenden Funktionen und grafischen Elemente stark unterstützt.
• Fenster
Für die Darstellung eines Programms sowie der Dialogelemente ist das Fenster das
zentrale Element.
(Bild: Programmfenster von Excel)
Es gibt etliche verschiedene Fenstertypen unter Windows, von denen Sie Ihre
Fenster ableiten können. Hauptfenster sind spezielle Ausführungen der
Fensterklassen, die überall auf dem Bildschirm erscheinen dürfen. Ein anderes
Beispiel sind Dialog- und Hilfefenster eines Programms. Kindfenster (WS_CHILD)
sind nur innerhalb der Fensterfläche ihres Elternfensters anzuordnen. Wenn sie
darüber herausragen sollten, werden sie abgeschnitten. Controls sind vordefinierte
Fensterklassen, die vor allem zur Gestaltung der Dialogfenster genutzt werden.
Name
Beschreibung
BUTTON
COMBOBOX
EDIT
LISTBOX
MDICLIENT
SCROLLBAR
STATIC
Schaltfläche
Kombinationslistenfeld
Eingabefeld
Listenfeld
MDI-Fenster
Rollbalken
Statisches Element
•
Menüs
Durch das Menüsystem wird der Benutzer zu den verschiedenen Möglichkeiten des
Programms geleitet. Dadurch entfällt das früher bei PC-Programmen nötige
Auswendiglernen der Befehle des Programms.
OOM/OOP
173
Um den Aufbau eines Menüs und vor allem seine Abarbeitung braucht der
Programmierer sich kaum zu kümmern. Er muss lediglich definieren, welche
Menüpunkte dargestellt werden sollen, welche Befehlsnummern mit einem
Menüpunkt in Zusammenhang stehen und die Funktionen schreiben, die für die
Ausführung der Menübefehle zuständig sind.
Um die Darstellung, die Reaktion auf die Maus und die Tastatur und die richtige
Verteilung der Befehle kümmert sich Windows.
Menüs werden in der Ressourcen-Datei (.rc) beschrieben.
•
Dialoge
Frühere Programme haben sich mit dem Benutzer meist nur über sehr kurze
Bildschirmausgaben ‘unterhalten’ sowie oftmals keine Hilfestellung bei ihrem Aufruf
gegeben.
Die Verbesserung, die Windows hier bietet, sind Dialoge.
Dialoge können aus beliebig vordefinierten Fensterklassen (Controls) gestaltet
werden, die der Kommunikation mit dem Anwender dienen.
Mit diesen Dialogen kann auch eine Meldung ausgegeben werden, die zur Kenntnis
genommen wird (OK) oder zum Abbrechen der kritischen Handlung führt (Weiter,
Abbrechen).
Die Ausgabe erfolgt in kleinen Fenstern mit festem Rahmen (MessageBox).
Prinzipiell gibt es drei Sorten von Dialogen:
1. Nicht modale Dialoge
die es zulassen, dass Sie, ohne sie zu beachten mit der Arbeit fortfahren. Die
Anwendung wird also nicht blockiert, bis Sie das Dialogfenster wieder
geschlossen haben.
2. Modale Dialoge
blockieren dagegen die Anwendung, bis sie wieder geschlossen sind. Ein
Beispiel dafür ist der Dateiauswahl-Dialog.
3. Systemmodale Dialoge
halten das gesamte System an, bis der Anwender das Dialogfenster bedient hat.
•
Weitere grafische Elemente
174
OOM/OOP
Windows stellt Ihnen neben diesen Hauptelementen für die Benutzeroberfläche noch
einige weitere Bestandteile zur Verfügung, die das Programm funktionaler gestalten
können:
OOM/OOP
-
175
Symbolbilder
Die Symbolbilder (Icons) können benutzt werden, um in einer Symbolleiste Befehle
zu repräsentieren. In diesem Fall ist das Programm so aufgebaut, dass es nach
einem Anklicken des Symbols eine bestimmte Aktion startet.
-
Maus-Cursor
Der Maus-Cursor hat die Standardform eines Pfeils, kann aber bei Bedarf angepasst
werden. Er verleiht dem Programm eine höhere Aussagekraft.
-
Bitmap
Das wichtigste grafische Gestaltungselement ist das Bitmap. Meist handelt es sich
dabei um zu dem Programm geladene Bilder im Paintbrush-Format (.BMP, .PCX)
oder in einer speziellen Form kodierte Grafikdateien.
-
Graphic Device Interface (GDI)
→ geräteunabhängige Grafikschnittstelle
Windows beinhaltet eine Grafikprogrammiersprache (GDI) über die sich auf einfache
Weise Grafiken und formatierter Text anzeigen lassen.
-
Hilfesystem
API – Schnittstelle zum Betriebssystem
→ mehrere 1000 Funktionen
8.3
Interne Abläufe
Im Gegensatz zu klassischen Programmen ist der Programmaufbau
folgendermaßen: nicht das Programm, sondern der Anwender bestimmt die
Reihenfolge seiner Arbeitsschritte.
Windows-Programme sind ereignisorientiert. Alle Informationen werden durch
Meldungen, die durch ein Maus- oder Tastaturereignis oder durch andere Fenster
ausgelöst werden, ausgetauscht.
Eine Mausbewegung oder ein Klick verursacht eine Meldung, die für das Fenster
gedacht ist, welches gerade das Zugriffsrecht auf die Maus besitzt. Eine
Tastatureingabe wird durch das Fenster entgegengenommen, welches den Fokus
(Aufmerksamkeit) von Windows besitzt.
Die Meldungen werden zu einem Paket zusammengeschnürt, das Auskunft darüber
gibt, welches Fenster von der Meldung betroffen ist und welche Meldung
eingegangen ist.
176
OOM/OOP
• Die Anwendungswarteschlange
Für jedes Fenster wird eine eigene Warteschlange für eingehende Meldungen
erzeugt. Jede das Fenster direkt betreffende Meldung, die nicht zu einem direkten
Aufruf der Fensterfunktion führt, wird in sie eingehängt. Der gängige Name für diese
Warteschlange ist Application Message Queue.
• Die Systemwarteschlange
Neben den einzelnen Warteschlangen der Fenster existiert noch eine dem gesamten
System zugeordnete Warteschlange, die System Message Queue.
8.4
Windows-Objekte und Handles
In der Windows-Programmierung spricht man oft von Objekten. Dabei handelt es
sich um Bestandteile eines Windowsprogramms, wie beispielsweise Fenster,
Speicher usw.
Um ein Objekt benutzen zu können, müssen Sie zuerst seine Definition angeben und
erhalten dann einen Integerwert, der das neue Objekt referenziert. Diesen Wert
nennt man Handle. Intern ordnet Windows dieser eindeutigen Nummer alle
benötigten Speicherressourcen und sonstigen Systemressourcen zu.
8.5
Grundlogik eines Windows-Programms
Jedes Windows-Programm funktioniert prinzipiell nach dem gleichen Schema:
1. Anmelden und Definieren der Fensterklasse des Hauptfensters vor der ersten
Benutzung, sowie der Fensterklassen anderer im Verlauf des Programms
benötigter Fensterobjekte.
2. Initialisierung des Programmfensters.
3. Bearbeitung der Application Message Queue, das heißt der entnehmen und
vorbereiten der darin befindlichen Meldungen.
4. In der Bearbeitungsfunktion des Hauptfensters stehen dann als große
Fallunterscheidung die Funktionsaufrufe und Anweisungen, mit denen das
Programm auf Ereignisse reagiert.
5. Ausgehend von diesem Hauptfenster wird in die weiteren Funktionen des
Programms verzweigt und die Ausgaben werden angezeigt.
6. Wenn eine Reaktion auf ein Benutzerereignis erfolgt ist, kehrt das Programm
wieder zur Meldungsschleife zurück, und das nächste vorhandene Ereignis kann
nach dem gleichen Schema abgearbeitet werden.
OOM/OOP
177
Maus
Tastatur
Timer
Windows
Ereignisse
Zyklischer
Ereignispuffer
Windows
versendet
Meldungen
Meldungen
Anwendung 1
Funktion
1
Funktion
2
Anwendung 2
Funktion
3
Funktion
1
Funktion
2
Grafische Ausgaben
GDI
Befehle für Peripherie
Bildschirm
Drucker
Bild: Funktionsschema API
8.6
Dateien
Dateierweiterung
Beschreibung
.CPP(.C)
.H
.MAK/.PRJ
.RC
.DEF
.EXE
Quelldatei(en)
Header-Datei(en)
Projektdatei(en)
Ressourcen-Datei(en)
Definitionsdatei(en)
ausführende Dateien
Funktion
3
178
8.7
OOM/OOP
Erstes Windows-Programm
Diese erste Beispielprogramm heißt FIRSTWINPROG und legt ein Fenster an, indem
der Textstring “Hello, Windows 95“ angezeigt wird. Bei den meisten dieser folgenden
Programmzeilen handelt es sich um einen notwendigen “Overhead“. Jedes
geschriebene Windows-Programm (reine API-Schnittstelle) wird einen ähnlichen
Überbau erhalten.
Der HELLOWIN.C Quellcode:
//******************************************************************
//*
//* Erstes Windows-Programm
//* OOM / OOP Übung
//* HELLOWIN.C
//*
//******************************************************************
#include <windows.h>
//******************************************************************
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
//******************************************************************
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static char szAppName[] = "HelloWin" ;
HWND hwnd ;
MSG msg ;
WNDCLASSEX wndclass ;
//---------------------------------------------------------------------wndclass.cbSize = sizeof (wndclass) ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH)
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION) ;
OOM/OOP
179
//---------------------------------------------------------------------RegisterClassEx (&wndclass) ;
//---------------------------------------------------------------------hwnd = CreateWindow (szAppName,
// window class
"The Hello Program",
// window caption
WS_OVERLAPPEDWINDOW,
// window style
CW_USEDEFAULT,
// initial x position
CW_USEDEFAULT,
// initial y position
CW_USEDEFAULT,
// initial x size
CW_USEDEFAULT,
// initial y size
NULL,
// parent window handle
NULL,
// window menu handle
hInstance,
// program instance handle
NULL) ;
// creation parameters
//---------------------------------------------------------------------ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
//---------------------------------------------------------------------while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
} // Ende „int WINAPI WinMain”
//******************************************************************
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam,
LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
//---------------------------------------------------------------------switch (iMsg)
{
case WM_PAINT :
180
OOM/OOP
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, "Hello, Windows 95!", -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;
} // Ende „LRESULT CALLBACK WndProc“
8.8
Erläuterungen zur C-Sourcecode-Datei
Die Datei besteht aus zwei Funktionen: WinMain und WndProc. WinMain ist der
Einstiegspunkt in das Programm und entspricht der Standard-C-Funktion main.
Jedes Windows-Programm besitzt eine WinMain-Funktion. WndProc ist die
Fensterprozedur für das HELLOWIN-Fenster. Jedes Fenster - egal ob so groß wie
das Hauptanwendungsfenster eines Programms oder so klein wie eine Schaltfläche
– verfügt über eine mit ihm verbundene Fensterprozedur. Die Fensterprozedur ist
eine Möglichkeit, um den Code zusammenzufassen, der auf Eingaben reagiert und
die Grafikausgabe am Bildschirm anzeigt. In HELLOWIN.C ist kein Code enthalten,
der WinProc direkt aufruft: WinProc wird nur von Windows aufgerufen. Allerdings gibt
es in WinMain eine Referenz auf WinProc. Dies ist auch der Grund, warum die
Funktion ziemlich am Anfang des Programms noch vor WinMain deklariert wird.
•
Die Windows-Funktionsaufrufe
HELLOWIN.C ruft 16 Windows-Funktionen auf. Es handelt sich dabei um folgende
Funktionen:
-
LoadIcon – lädt ein Icon, das in einem Programm verwendet wird
LoadCursor – lädt einen Maus-Cursor, der in einem Programm verwendet wird
GetStockObject – beinhaltet ein Grafikobjekt
RegisterClassEx – registriert eine Fensterklasse für das Programmfenster
CreateWindow – erstellt ein Fenster auf Basis einer Fensterklasse
ShowWindow – zeigt das Fenster am Bildschirm an
UpdateWindow – weist das Fenster an, sich selbst zu zeichnen
GetMessage – holt eine Meldung aus der Meldungswarteschlange
TranslateMessage – übersetzt einige Tastaturmeldungen
DispatchMessage – schickt eine Meldung an die Fensterprozedur
BeginPaint – löst den Zeichenvorgang des Fenster aus
GetClientRect – ruft die Ausmaße des Fensterinnenbereiches ab
DrawText – zeigt einen Textstring an
OOM/OOP
181
EndPaint – beendet den Zeichenvorgang des Fensters
PostQuitMessage – fügt eine “Beenden“-Meldung in die Meldungswarteschlange
ein
DefWindowProc – führt die Standardverarbeitung der Meldungen aus
-
-
Diese Funktionen sind in der Online-Hilfe dokumentiert und werden über
WINDOWS.H in verschiedenen Header-Dateien deklariert.
•
Großgeschriebene Bezeichner
In HELLOWIN.C gibt es eine Reihe von großgeschriebenen Bezeichnern. Diese
Bezeichner sind in den Windows-Header-Dateien definiert. Etliche dieser Bezeichner
bestehen aus einem Präfix mit zwei oder drei Buchstaben, gefolgt von einem
Unterstrich:
CS_HREDRAW
CS_VREDRAW
WM_CREATE
WM_PAINT
WM_DESTROY
usw.
Es handelt sich hier um einfache numerische Konstanten. Das Präfix weist auf die
allgemeine Kategorie hin zu der die Konstante gehört:
Präfix
CS
IDI
IDC
WS
CW
WM
SND
DT
•
Kategorie
Klassenstil
ID-Nummer für ein Icon
ID-Nummer für einen Cursor
Fensterstil
Fenster erstellen
Fenstermeldung
Sound-Option
Text zeichnen
Neue Datentypen
Bei einigen weiteren in HELLOWIN.C verwendeten Bezeichnern handelt es sich um
neue Datentypen, die ebenfalls in den Header-Dateien definiert sind. Manchmal sind
diese neuen Datentypen lediglich praktische Abkürzungen.
z.B.: UNIT → unsigned int
LONG → 32-Bit-signed long Integerwert
Bei anderen Datentypen ist der Sinn nicht ganz so offensichtlich. So gibt die Funktion
WndProc einen Wert des Typs LRESULT zurück. Dieser ist ganz einfach als LONG
definiert. Die WinMain-Funktion ist vom Typ WINAPI (wie alle Windows-Funktionen)
und die WinProc-Funktion ist vom Typ CALLBACK. Beide Bezeichner sind als
_stdcall definiert und beziehen sich auf eine spezielle Aufrufsequenz für
Struktur
MSG
WNDCLASSEX
PAINTSTRUCT
Bedeutung
Struktur für Meldungen
Struktur für Fensterklassen
Struktur zum Zeichnen
182
OOM/OOP
Funktionsaufrufe, die zwischen Windows selbst und ihrer Anwendung auftreten.
Außerdem verwendet HELLOWIN.C vier Datenstrukturen:
• Handles
Handles werden in Windows sehr häufig verwendet. Ein Handle ist ein Zahl
(gewöhnlich ein 32-Bit-Wert), die sich auf ein Objekt bezieht. Der tatsächliche Wert
des Handle ist für das Programm unwichtig, aber das Windows-Modul, das dem
Programm das Handle übergibt, weiß, wie es als Referenz auf das Objekt zu
verwenden ist.
Bezeichner
HINSTANCE
HWND
HDC
•
Bedeutung
Handle auf eine Instanz - das eigentliche Programm
Handle auf ein Fenster
Handle auf einen Gerätekontext
Ungarische Notation
→ Konvention zur Benennung von Variablen
Dabei beginnt der Name einer Variablen ganz einfach mit einem oder mehreren
kleingeschriebenen Buchstaben, die den Datentyp der Variable kennzeichnen. Bei
der Benennung von Strukturvariablen wird der kleingeschriebene Strukturname
entweder als Präfix für den Variablennamen oder als gesamter Variablennamen
verwendet.
Präfix
c
by
n
i
b
w
l
s
fn
sz
h
p
•
Datentyp
char
BYTE
short
int
BOOL
WORD
LONG
string
function
string
handle
pointer
Der Programmeinstiegspunkt
Der Einstiegspunkt eines Windows-Programms ist immer eine Funktion namens
WinMain:
int WINAPI WinMain (HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR szCmdLine,
int iCmdShow)
// Instanz-Handle
// Instanz-Handle (meistens 0)
// für Befehlszeilenparameter
// Wie wird das
// Fenster angezeigt?
OOM/OOP
183
184
•
OOM/OOP
Registrierung der Fensterklassen
Ein Fenster wird immer auf Basis einer Fensterklasse erstellt. Die Fensterklasse
bestimmt die Fensterprozedur, die Meldungen an das Fenster verarbeitet. Auf der
Grundlage einer einzelnen Fensterklasse kann mehr als ein Fenster erstellt werden.
Beispielsweise werden sämtliche Schaltflächenfenster in Windows auf Basis
derselben Fensterklasse erstellt. Die Fensterklasse legt die Fensterprozedur und
einige weitere Merkmale des Fensters fest.
Durch den Aufruf der Funktion RegisterClassEx wird eine Fensterklasse registriert.
Die RegisterClassEx-Funktion benötigt als Parameter nur einen Zeiger auf eine
Struktur des Typs WNDCLASSEX.
→ Die Struktur ist wie folgt definiert:
typedef struct _WNDCLASSEX
{
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
}
WNDCLASSEX;
→ Definition einer Struktur des Typs WNDCLASSEX:
WNDCLASSEX wndclass;
→ Definition der 12 Felder der Struktur:
wndclass.cbSize
wndclass.style
wndclass.lpfnWndProc
wndclass.cbClsExtra
wndclass.cbWndExtra
wndclass.hInstance
wndclass.hIcon
wndclass.hCursor
wndclass.hbrBackground
wndclass.lpszMenuName
wndclass.lpszClassName
wndclass.hIconSm
= sizeof (wndclass) ;
= CS_HREDRAW | CS_VREDRAW ;
= WndProc ;
=0;
=0;
= hInstance ;
= LoadIcon (NULL, IDI_APPLICATION) ;
= LoadCursor (NULL, IDC_ARROW) ;
= (HBRUSH) GetStockObject (WHITE_BRUSH) ;
= NULL ;
= szAppName ;
= LoadIcon (NULL, IDI_APPLICATION) ;
OOM/OOP
185
Die beiden wichtigsten Felder sind das zweitletzte und das dritte. Das zweitletzte
Feld ist der Name des Fensterklasse (wird im allgemeinen auf den Programmnamen
festgelegt). Das dritte Feld ist die Adresse der Fensterprozedur, die für alle Fenster
verwendet wird, die auf Basis dieser Klasse (also der Funktion WndProc in
HALLOWIN.C) erstellt werden. Die anderen Felder geben die Merkmale aller Fenster
auf Basis dieser Fensterklasse wieder.
•
Erstellung des Fensters
Ein Fenster wird durch den Aufruf von CreateWindow erstellt. Dabei benötigt der
Aufruf (im Gegensatz zu einer Datenstruktur wie RegisterClassEx) sämtliche zu
übergebenden Informationen als Parameter der Funktion.
hwnd = CreateWindow (szAppName,
"The Hello Program",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL) ;
// Name der Fensterklasse
// Fenstertitel
// Fensterstil (Standard)
// anfängliche X-Position
// anfängliche Y-Position
// anfängliche X-Größe
// anfängliche Y-Größe
// Handle des Eltern-Fensters
// Handle des Fenster-Menüs
// Handle der Programminstanz
// Erstellungsparameter
Der Aufruf CreateWindow gibt ein Handle auf das erstellte Fenster zurück. Dieses
Handle wird in der Variablen hwnd abgelegt, die vom Typ HWND definiert ist. Jedes
Fenster in Windows besitzt ein Handle. Das Programm verwendet das Handle, um
sich auf das Fenster zu beziehen.
•
Anzeige des Fensters
Nachdem das Fenster intern erzeugt wurde, wird es durch 2 weitere Aufrufe am
Bildschirm angezeigt. Der erste ist:
ShowWindow(hwnd,
// Handle auf das erstellte Fenster
iCmdShow); // Wie wird das Fenster angezeigt?
// SW_SHOWNORMAL oder SW_SHOWMINNOACTIVE
Der zweite ist:
UpdateWindow(hwnd);
Durch diesen Aufruf wird veranlasst, dass der Innenbereich aufgebaut wird.
186
•
OOM/OOP
Die Meldungsschleife
Nachdem das Fenster vollständig am Bildschirm sichtbar ist, muss das Programm
jetzt darauf vorbereitet werden, Tastatur- und Mauseingaben des Anwenders
aufzunehmen. Für jedes aktuell unter Windows ausgeführte Programm verwaltet
Windows eine Meldungswarteschlange. Falls eine Eingabe auftritt, übersetzt
Windows diesen Vorgang in eine Meldung, die es in die Meldungswarteschlange des
Programms übernimmt. Ein Programm holt diese Meldungen aus der
Meldungswarteschlange ab, indem es einen als Meldungsschleife bezeichneten
Codeblock ausführt:
MSG msg;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
GetMessage()
→ Holen der Nachricht aus der Warteschleife
TranslateMessage → Erkennen der Zuordnung der Nachricht
DispatchMessage → Verteilen der Nachricht an die Fensterfunktionen
•
Die Fensterprozedur
Die Fensterprozedur legt nun fest, was das Fenster in seinem Innenbereich anzeigen
und wie das Fenster auf Anwendungseingaben reagieren soll. In FIRSTWINPROG
handelt es sich bei der Fensterprozedur um die Funktion WndProc. Eine
Fensterprozedur kann einen beliebigen Namen besitzen. Ein Windows-Programm
kann mehr als eine Fensterprozedur besitzen, wobei diese immer mit einer
bestimmten Fensterklasse verknüpft ist, die durch den Aufruf von RegisterCallEx
registriert wird. Eine Fensterprozedur wird immer folgendermaßen definiert:
LRESULT CALLBACK WndProc (HWND hwnd,
UINT iMsg,
WPARAM wParam,
LPARAM lParam)
•
// Handle auf das Fenster
// Nummer, die die
// Meldung identifiziert
// Meldungsparameter
// Meldungsparameter
Verarbeitung der Meldungen
Jede Meldung, die eine Fensterprozedur empfängt, wird durch eine Nummer
identifiziert, die dem iMsg-Parameter der Fensterprozedur entspricht. Gewöhnlich
benutzen Windows-Programmierer eine switch- und case-Konstruktion, um
festzulegen, welche Meldung wie zu verarbeiten ist. Wenn eine Fensterprozedur eine
Meldung verarbeitet, sollte sie eine 0 zurückgeben. Sämtliche Meldungen, die nicht
verarbeitet werden, müssen an ein Windows-Funktion namens DefWindowProc
übergeben werden. So werden auch Meldungen verarbeitet, die nicht von der
Fensterprozedur behandelt wurden.
OOM/OOP
•
187
Die WM_PAINT- Meldung
Diese Meldung ist für die Windows-Programmierung außerordentlich wichtig. Sie
informiert ein Programm, sobald der Innenbereich eines Fensters ungültig geworden
ist und daher neu gezeichnet werden muss. Dies ist zum Beispiel bei der ersten
WM_PAINT-Meldung der Fall oder wenn die Größe des FIRSTWINPROG-Fensters
verändert wird. Sobald WinProc die WM_PAINT-Meldung erhält, wird folgender
Programmcode ausgeführt:
hdc = BeginPaint (hwnd,
&ps) ;
// Handle auf das Fenster
// Informationen zur Innenbereichsdarstellung
GetClientRect (hwnd, &rect) ;
// Bestimmung der Größe des Innenbereichs
DrawText (hdc,
// Handle auf Gerätekontext
"Hello, Windows 95!", // der zu zeichnende Text
-1,
// Textstring mit 0-Byte abgeschlossen
&rect,
// Innenbereichsabmessungen
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
// Informationen zur Platzierung ( einzelne Zeile, zentriert)
EndPaint (hwnd, &ps) ;
// Ende der WM_PAINT-Meldung
Der Aufruf von BeginPaint liefert ein Handle für einen Gerätekontext als
Rückgabewert. Ein Gerätekontext bezieht sich auf ein physikalisches Ausgabegerät
(z.B. Bildschirm) und dessen Gerätetreiber. Das Gerätekontext-Handle wird benötigt,
um Text und Grafiken innerhalb des Innenbereiches eines Fensters anzuzeigen.
DrawText zeigt dann den Text an.
•
Die WM_DESTROY- Meldung
Diese Meldung ist ein Ergebnis davon, dass der Benutzer die Schaltfläche
„Schließen“ angeklickt, die Option „Schließen“ aus dem Systemmenü des
Programms gewählt oder die Tastenkombination Alt+F4 gedrückt hat.
FIRSTWINPROG reagiert standardmäßig auf diese Meldung durch den Aufruf von:
PostQuitMessage(0);
Diese Funktion fügt die Meldung WM_QUIT der Meldungswarteschlange hinzu.
Wenn GetMessage die Meldung WM_QUIT erhält, gibt GetMessage den Wert 0
zurück. Dies bedingt, dass WinMain aus der Meldungsschleife aussteigt und das
Programm beendet.
188
OOM/OOP
9 Literatur
1) Programmieren in C
Günther Lamprecht
2) Objektorientiertes Programmieren in C++
Nikolai Josultis, Addison-Wesley
3) C++ - Standardbibliothek
Nikolai Josultis, Addison-Wesley
4) C++ Kurzgefasst
R. Krienke
5) C++ - Standardbibliothek
Nikolai Josuttis, Addison-Wesley
6) Visual C++ in 21 Tagen
Ori Gurewich, Nathan Gurewich
7) Booch, Grady: Objektorientierte Analyse und Design. Bonn, Paris, Reading,
Mass. u.a.: Addison-Wesley, 1994
8) Coad, Yourdon: Object-oriented analysis, Prentice-Hall, 1991
9) Coad, Yourdon: Object-oriented design, Prentice-Hall, 1991
10) Diaz, J.: Objektorientierte Modellierung geotechnischer Ingenieursysteme. Forum
Bauinformatik "Junge Wissenschaftler forschen". VDI-Verlag Reihe 20 Nr. 173,
1995
11) Heuer, A.: Objektorientierte Datenbanken. Bonn, München, Paris, u.a.: AddisonWesley, 1992
12) Hinz, O.: Ein objektorientiertes Modell für Entwurf und Berechnung von
Grundbaukonstruktionen. Forum Bauinformatik "Junge Wissenschaftler
forschen". VDI-Verlag Reihe 20 Nr. 131, 1994
OOM/OOP
189
13) Jell, Thomas, von Reeken, J.: Objektorientiertes Programmieren mit C++. 2.
bearb. und erw. Aufl.. München, Wien: Carl Hanser Verlag, 1993
14) Lennerts, K.: Objektorientierte Modellierung der Baustelle unter dem
Gesichtspunkt des Materialflusses. Forum Bauinformatik "Junge Wissenschaftler
forschen". VDI-Verlag Reihe 20 Nr. 99, 1993
15) Lennerts, K.: Objektorientierter Entwurf von ESBE - Eine Vorgehensweise.
Forum Bauinformatik "Junge Wissenschaftler forschen". VDI-Verlag Reihe 20 Nr.
131, 1994
16) POET 2.1 Programmers & Reference Guide. Hamburg: POET Software GmbH,
1993
17) Rumbaugh, Blaha, Premerlani, Eddy, Sorensen: Object Oriented Modeling and
Design, Prentice-Hall, 1991
18) Rüppel, U.: Integration von Teilprozessen des Bauplanungsprozesses mit
objektorientierten Schnittstellen basierend auf STEP-2DBS. Forum Bauinformatik
"Junge Wissenschaftler forschen". VDI-Verlag Reihe 4 Nr. 116, 1992
19) Rüppel, U.: Objektorientierter Datenaustausch zwischen Entwurfs- und
Tragwerksplaner. Forum Bauinformatik "Junge Wissenschaftler forschen". VDIVerlag Reihe 20 Nr. 99, 1993
20) Rüppel, U.: Produktmodellierung im Bauwesen. Forum Bauinformatik "Junge
Wissenschaftler forschen". VDI-Verlag Reihe 20 Nr. 131, 1994
21) Schäfer, Steffen: Objektorientierte Entwurfsmethoden: Verfahren zum S
objektorientierten Softwareentwurf im Überblick. Bonn; Paris, Reading, Mass
u.a.: Addison-Wesley, 1994
22) Vetter, M.: Objektmodellierung. Stuttgart: B.G. Teubner, 1995
23) Diaz, J.: Objektorientierte Modellierung geotechnischer Ingenieursysteme. Forum
Bauinformatik "Junge Wissenschaftler forschen". VDI-Verlag Reihe 20 Nr. 173,
1995
24) Stroustrup, B.: Die C++ Programmiersprache. 2. überarbeitete Auflage. Bonn,
München, Paris u.a: Addison-Wesley, 1992
Herunterladen