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