Softwaretechnik in C und C++ - Das Kompendium Modulare, objektorientierte und generische Programmierung Bearbeitet von Rolf Isernhagen 3. Auflage 2001. Buch. XXVI, 994 S. Hardcover ISBN 978 3 446 21726 3 Format (B x L): 18,2 x 24,5 cm Gewicht: 1759 g Zu Inhaltsverzeichnis schnell und portofrei erhältlich bei Die Online-Fachbuchhandlung beck-shop.de ist spezialisiert auf Fachbücher, insbesondere Recht, Steuern und Wirtschaft. Im Sortiment finden Sie alle Medien (Bücher, Zeitschriften, CDs, eBooks, etc.) aller Verlage. Ergänzt wird das Programm durch Services wie Neuerscheinungsdienst oder Zusammenstellungen von Büchern zu Sonderpreisen. Der Shop führt mehr als 8 Millionen Produkte. Kapitel 1 ANSI/ISO-C, Clean-C und Better-C I think C has a lot of features that are very important. The way C handles pointers, for example, was a brilliant innovation; ... I do like C as a language, especially because it blends in with the operating system (if you’re using UNIX, for example). Donald E. Knuth, 1993, CLB Interview The very features of C that make it an unsuitable candidate for human consumption – lack of structure, weak typing, ... – strengthen its appeal as a “universal assembly language” ... Bertrand Meyer, 1998, JOOP 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 1.12 1.13 1.14 1.15 1.16 Entwicklungsgeschichte: Von K&R-C nach C++ Clean-C und Better-C . . . . . . . . . . . . . . . Neun Beispiele . . . . . . . . . . . . . . . . . . . Aufbau der Sprache . . . . . . . . . . . . . . . . . Daten, Operatoren, Ausdrücke, Anweisungen . Steueranweisungen . . . . . . . . . . . . . . . . . Funktionen . . . . . . . . . . . . . . . . . . . . . . Datenstrukturen, Werte- und Zeigersemantik . Eingabe und Ausgabe von Daten . . . . . . . . Programmstruktur und Speicherklassen . . . . Zeiger auf Funktionen . . . . . . . . . . . . . . . Präprozessor . . . . . . . . . . . . . . . . . . . . . Speicherverwaltung, Speichermodelle . . . . . Fehlerbehandlung/Ausnahmebehandlung . . . Übersicht über den neuen Standard C99 . . . . Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 6 7 17 21 44 54 67 110 128 148 152 158 160 163 167 2 1 ANSI/ISO-C, Clean-C und Better-C Inzwischen gibt es zwei C-Standards. Der alte Standard, kurz C89, stammt aus dem Jahre 1989, der neue, kurz C99, aus dem Jahre 1999. Der neue Standard ist eine Erweiterung des alten, er hat bisher keine breite Unterstützung bei den CompilerHerstellern gefunden. Ein Grund dafür ist sicher die Tatsache, dass viele der Erweiterungen nicht C++-konform sind. In den folgenden Abschnitten 1.1 bis 1.14 wird C89 zu Grunde gelegt. Alle dort aufgeführten C-Texte entsprechen dem alten und dem neuen Standard! Die Erweiterungen von C99 gegenüber C89 werden im Abschnitt 1.15 vorgestellt. 1.1 Entwicklungsgeschichte: Von K&R-C nach C++ C ist eine gewachsene Sprache, die sich über einen langen Zeitraum zum heutigen ANSI/ISO-C entwickelt hat. Sie ist im Vergleich zu einigen anderen Sprachen wie z.B. Pascal, Ada, Modula-2 oder Oberon nicht besonders systematisch aufgebaut und teilweise recht kryptisch, aber sehr verbreitet in der Industrie und auch im Hochschulbereich. C reicht in etwa von der Assembler-Ebene bis zur Ebene der klassischen höheren Sprachen und – mit der inzwischen ebenfalls standardisierten Erweiterung C++ – bis zur Ebene der objektorientierten Sprachen. Wesentliche Nachteile von C: • nicht durchweg systematischer Aufbau (auch wegen Kompatibilität mit K&R-C) • nicht konsequent statisch typisiert • schwache Unterstützung für modulare Programmierung • teilweise ins Kryptische gehende Notation Wesentliche Vorteile von C: • standardisiert und sehr stark verbreitet • hohe Bandbreite (Assembler-Ebene bis zur Ebene der Hochsprachen) • speicher- und laufzeiteffizient • flexibel und anpassungsfähig Die Sprache C – und insbesondere C++ – ist kompliziert wie das reale Leben, sie hat begeisterte Freunde und leidenschaftliche Gegner. Auch hier bietet sich ein Vergleich aus einem anderen Bereich an. Englisch ist eine Weltsprache: wer global kommunizieren will, muss Englisch lesen, sprechen und schreiben, egal ob ihm die Sprache gefällt oder nicht. Es gibt viele C-Dialekte, Entwicklungsstufen und Untermengen, Bild 1.1 gibt einen Überblick über das Wesentliche. Das klassische Standardwerk zu K&R-C/ANSI-C ist [1]. Hier eine kurze Skizze der Entwicklung von K&R-C nach ANSI/ISO-C++ : 1.1 3 Entwicklungsgeschichte: Von K&R-C nach C++ Bild 1.1: Entwicklungsstufen der Programmiersprache C • K&R-C: klassischer Standard seit 1978, definiert von Kernighan und Ritchie • ANSI-C: ANSI-Standard seit Dezember 1989 (kurz: C89), beinhaltet wesentliche Erweiterungen gegenüber K&R-C: – Strengere statische Typisierung, Funktionsprototypen – Datentyp void* für nicht typisierte Zeiger (Adressen) – Kopieren und Zuweisen von Strukturtypen – Attribut const zur Definition von Konstanten, Aufzählungstypen • Better-C: Untermenge von C++, und zwar C++ ohne Klassen, wesentliche Erweiterungen gegenüber C: – weiter verbesserte statische Typisierung – elegantere Ein- und Ausgabe mit den überladenen Operatoren << und >> – Referenztypen, Überladen von Funktionen und Operatoren – vereinfachte dynamische Speicherverwaltung mit new und delete – Inline-Kommentare und Inline-Funktionen – Namensbereiche (name spaces) • C++: Von Stroustrup entwickelte objektorientierte Programmiersprache als Obermenge von C, seit September 1998 ebenfalls nach ANSI/ISO standardisiert. Wesentliche Erweiterungen gegenüber Better-C: – Klassenkonzept zur Realisierung Abstrakter Datentypen (ADT) – Vererbungsmechanismus (Inheritance) zur Erweiterung von Klassen – dynamische Bindung von Element-Funktionen (Polymorphismus) – generische Konstrukte durch Verwendung von Schablonen (Templates) – Ausnahmebehandlung (Exception Handling) 4 1.2 1 ANSI/ISO-C, Clean-C und Better-C Clean-C und Better-C ANSI/ISO-C schleppt – insbesondere wegen seiner Abwärtskompatibilität zu K&R-C – einiges an Ballast mit sich herum, den ANSI/ISO-C++ abgeworfen hat, d.h. es besteht keine 100%ige Aufwärtskompatibilität. Bild 1.2 stellt den Zusammenhang dar: • Als Clean-C wird im Weiteren der Sprachbereich bezeichnet, der sich aus der Schnittmenge der Sprachbestandteile von ANSI/ISO-C und ANSI/ISO-C++ ergibt, d.h. Clean-C ist ein etwas eingeschränktes ANSI/ISO-C und • Clean-C-Programme sind praktisch gesehen Programme, die einen ANSI/ISOC-Compiler und auch einen ANSI/ISO-C++-Compiler ohne Fehlermeldung passieren. mkno0pqrm !" #$" !&%')(+**-,.0/21-!35476089.0:<; =>=71?A@B54%DCFE71GF=7,4; 1/H=; 14I5/2J K LM5NOQPKR S&T9U0V0W X>XYZV9[0Z]\2^V`_ aScb d$b Se`f)gih0V9jD_kaSb d$b S&ef)g+ll Bild 1.2: Definition der Sprachsubsets Clean-C und Better-C Aus Gründen der Portierbarkeit, der Zukunftssicherheit, der Robustheit und der Erweiterbarkeit empfiehlt es sich sehr, bei der Entwicklung von C-Programmen nur Clean-C zu verwenden, d.h. nur solche Sprachmittel, die auch Bestandteil von C++ sind. Die Einschränkungen sind gering, sie werden jeweils am Ende eines Unterabschnitts zusammenfassend dargestellt. Diese Strategie ermöglicht einen gleitenden Übergang von C nach C++. ANSI/ISO-C++ enthält gegenüber ANSI/ISO-C nicht nur objektorientierte Erweiterungen, sondern darüber hinaus auch eine Reihe von nichtobjektorientierten Erweiterungen, die es in vielen Fällen ermöglichen, Zusammenhänge klarer und besser lesbar zu formulieren: • Als Better-C wird im Weiteren ein Sprachbereich bezeichnet, der eine Obermenge von Clean-C und gleichzeitig eine Untermenge von C++ darstellt (siehe Bild 1.2). 1.3 5 Neun Beispiele Better-C kann eine Zwischenstufe auf dem Wege von C nach C++ sein beim Erlernen der Sprache und auch in der Programmentwicklung. Die Grenze zwischen Better-C und C++ verläuft fließend und sie ist nicht durch Compiler überprüfbar. Die elementaren Better-C-Erweiterungen werden jeweils im Anschluss an die CleanC-Einschränkungen am Ende eines Unterabschnitts zusammengefasst, dadurch wird ebenfalls der Übergang von C nach C++ erleichtert. 1.3 Neun Beispiele Die folgenden neun Beispiele geben einen ersten Einblick in die Programmiersprache ANSI/ISO-C: • Beispiel 1: Hallo Welt • Beispiel 2: . . . und da ist die Welt • Beispiel 3: Dateilister Version 0 • Beispiel 4: Datei kopieren • Beispiel 5: Zahlensumme berechnen • Beispiel 6: Dateilister Version 1 • Beispiel 7: Dateilister Version 2 • Beispiel 8: Worthäufigkeiten bestimmen • Beispiel 9: Ein Programm, das sich selbst reproduziert Leser, die mit anderen höheren Programmiersprachen wie z.B. ALGOL, Pascal, Modula, Oberon oder Ada vertraut sind, werden viele Grundstrukturen wiedererkennen, das betrifft insbesondere die Ablaufstrukturen (Kontrollstrukturen). Eine gute Möglichkeit zum praktischen Einstieg in ANSI-C besteht darin, diese Beispiele in den Rechner einzugeben, sie ablaufen zu lassen und dann zu modifizieren und auch Fehler einzubauen, um zu sehen, wie der Compiler darauf reagiert. Das wohl bekannteste Programm der Welt ist auch das erste C-Programm-Beispiel in dem Standardwerk von Kernighan/Ritchie [1] und wird in C-Text 1.1 wiedergegeben. C-Text 1.1: Hallo Welt #include <stdio.h> int main(void ) /* Das erste Beispiel */ { printf("hallo, world\n"); return 0; } (./Bsp1/b1to7/b1.c) 6 1 ANSI/ISO-C, Clean-C und Better-C Erläuterungen zu C-Text 1.1: • Mit #include <stdio.h> wird das Modul für die Standardeingabe und -ausgabe eingefügt. Das Suffix h bedeutet Header-Datei (kurz: H-Datei ), was hier soviel wie Schnittstellen-Datei bedeutet. Es gibt 15 Standard-H-Dateien, z.B. stdio, string, float, math, ..., siehe auch [1] Anh. B (S. 239). • main bedeutet Hauptprogramm, genauer Hauptfunktion. Formal ist main eine integer-Funktion des Betriebssystems und gibt über die return-Anweisung bei Programmende den Wert 0 (d.h. 0 Fehler, alles okay) an das Betriebssystem zurück. • Das Klammerpaar { } steht für begin und end, Kommentare werden in /* und */ eingeschlossen, Kommentarschachtelungen sind nicht erlaubt! • printf(...) ist die Standardfunktion für formatierte Ausgabe aus stdio.h, \ zeigt an, dass ein Steuerzeichen folgt, n steht dort für Zeilenende. • Während in Sprachen wie z.B. Pascal, Modula und Oberon das Semikolon als Trennzeichen zwischen zwei Anweisungen steht, wird in C jede Anweisung durch ein Semikolon abgeschlossen, d.h. also, dass das Semikolon hinter return 0 zwingend notwendig ist. Das als C-Text 1.2 wiedergegebene Programm 1 ist als kleiner einführender Scherz gedacht. Als C-Anfänger sollte man nicht versuchen, es zu verstehen. Es generiert eine Weltkarte, wenn es z.B. mit b2 50 50 aufgerufen wird. C-Text 1.2: ... und das ist die Welt (./Bsp1/b1to7/b2.c) #include <stdlib.h> #include <stdio.h> main(l ,a,n,d)char **a;{ for (d=atoi(a[1])/10*80atoi(a[2])/5-596;n="@NKA\ CLCCGZAAQBEAADAFaISADJABBA^\ SNLGAQABDAXIMBAACTBATAHDBAN\ ZcEMMCCCCAAhEIJFAEAAABAfHJE\ TBdFLDAANEfDNBPHdBcBBBEA_AL\ H E L L O, W O R L D! " [l++-3];)for (;n-->64;) putchar(!d+++33^ l&1);} Das Programm erzeugt mit dem angegebenen Aufruf folgende Ausgabe: 1 Das Programm habe ich von Herrn Carsten Hoffmann, einem Hörer meiner Vorlesung. Er hat es in einer Mailbox gefunden 1.3 7 Neun Beispiele !!!!!!!!!!! !!! ! !!!!!!!!!!!!!!!!! !!!!! ! !!!!!!!!!!!!!!!!!!! !!!! !!!!!!!!!!!!!! !!!!!!!!! ! !!!! ! !!!!! !!!!! !!!!!!!! !!!!!! !!!! !! ! ! !!! !!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! !! !!!!!"!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!! !!!!!!!!!!!!!!!!! !! ! !! ! !!!!!!!!!!!!!!!!!!!! ! !!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!! !!! !!! ! !!!!!!!!!! ! ! ! ! !!!!! !! !!!! ! !!!!! !! !!!!!!!! !! !! ! Der C-Text 1.3 ist ein Programm, das die Textdatei b3.c – also den eigenen Quelltext – auf den Bildschirm ausgibt. C-Text 1.3: Dateilister Version 0 (./Bsp1/b1to7/b3.c) #include <stdio.h> int main(void ) /* Eine Textdatei auf den Bildschirm kopieren (Filelister) */ { FILE *fp; int c; fp = fopen("b3.c", "r"); c = getc(fp); while (c != EOF ) { putchar(c); c = getc(fp); } fclose(fp); return 0; } Erläuterungen zu C-Text 1.3: • fp (filepointer) ist ein Zeiger (eine Adresse), das Zeichen * ist der Inhaltsoperator (Inhalt von). • EOF bedeutet End Of File und ist als Konstante in stdio.h definiert. • Für Wertzuweisungen wird in C der Operator = verwendet, für Abfragen auf Gleichheit bzw. Ungleichheit werden die Operatoren == bzw. != verwendet. 8 1 ANSI/ISO-C, Clean-C und Better-C • Die Funktion getc(fp) liest ein Zeichen aus der durch fp bezeichneten Datei, die Funktion putchar(c) schreibt ein Zeichen auf das Standard-Ausgabemedium (Bildschirm), beide sind in stdio definiert. • Die Zeichenvariable c wird bewusst als int-Typ vereinbart, unter anderem, weil die in stdio.h definierte Konstante EOF nicht den Wert eines bestimmten Zeichens des definierten Zeichensatzes (z.B. des ASCII-Zeichensatzes) hat, sondern einen Wert außerhalb des entsprechenden Zeichensatzes. Ein typischer Wert für EOF ist z.B. -1. Aus diesem Grunde ist der Rückgabetyp der Funktion getc auch int und nicht char. • C erlaubt Klein- und Großschreibung für die Buchstaben innerhalb von Bezeichnern und ist case sensitive, d.h. es unterscheidet große und kleine Buchstaben. So sind z.B. meier, Meier und MEIER drei verschiedene Bezeichner. Das Programm C-Text 1.4 kopiert die Textdatei b4.c, also wieder den eigenen Quelltext. Die Datei mit dem kopierten Text erhält den Namen xxxxxx. C-Text 1.4: Datei kopieren (./Bsp1/b1to7/b4.c) #include <stdio.h> int main(void ) /* Kopieren einer Textdatei (Filecopy) */ { FILE *fpi, *fpo; int c; fpi = fopen("b4.c", "r"); fpo = fopen("xxxxxx", "w"); c = getc(fpi); while (c != EOF ) { putc(c, fpo); c = getc(fpi); } fclose(fpi); fclose(fpo); return 0; } Pn2 Das Programm C-Text 1.5 bestimmt jeweils die Summe S = i=n1 i für zwei eingelesene Werte n1 und n2 mit n1 ≤ n2 und führt die entsprechende Berechnung dazu in dem Unterprogramm (der C-Funktion) summe durch. 12.1 Klassendiagramme: Modellierung der Architektur 12.1 929 Klassendiagramme: Modellierung der Architektur Der wichtigste und meist verwendete Teil von UML ist sicher das Subset Klassendiagramme zur Modellierung der statischen Programmstruktur, d.h. der Architektur des Programmes. Damit werden die im Programm definierten Klassen sowie die Beziehungen zwischen den Klassen durch eine Modellierungssprache mit grafischen Elementen dargestellt. Bei der Verwendung von Entwicklungswerkzeugen zum Arbeiten mit UMLDiagrammen sind zwei Vorgehensweisen nützlich, die auch von den meisten Herstellern unterstützt werden: (1) Die wichtigste und primere Vorgehensweise ist die interaktive Erstellung eines Programmmodells (Modellbildung, Entwurf in Form eines Klassendiagrammes) unter Verwendung der dafür vorgesehenen Sprachelemente. (2) Die zweite auch sehr interessante Vorgehensweise beinhaltet die rückwärtsgerichtete Erzeugung eines Programmmodells aus einem C++-Text (reverse engineering). Zu (1) Diese Vorgehensweise repräsentiert den Programm-Entwurf – zumindest den Entwurf der Architektur – und führt zu der Programm-Spezifikation in Form von UML-Klassendiagrammen. Die Programmspezifikation ist einerseits eine verbindliche Vorgabe für die nachfolgende Implementierung und kann darüber hinaus später als Teil-Dokumentation des Programmes verwendet werden. Bild 12.1 stellt Entwurf und Spezifikation im Rahmen des klassischen Phasenmodells dar. Zu (2) Die umgekehrte Vorgehensweise (Reverse Engineering) – d.h. hier die Erzeugung von Klassendiagrammen aus C++-Code ist aus verschiedenen Sichten interessant. Eine ist die damit mögliche Analyse bestehenden Codes und die rückwirkende Erzeugung der Spezifikation für Dokumentationszwecke. Ein anderer Aspekt – insbesondere interessant in Verbindung mit der Sprache C++– besteht darin, durch eine Codeanalyse zu überprüfen, ob die Richtlinien der objektorientierten Programmierung eingehalten worden sind. Wichtiger Hinweis UML ist eine Sprache zur Modellierung objektorientierter Software, C++ unterstützt aber auch nicht objektorientierte Programmiermethoden, sehr häufig wird in C++ hybrid programmiert. Deshalb ist es sinnvoll, sich beim Start eines entsprechenden Softwareprojektes für eine bestimmte Programmiermethodik zu entscheiden. Will man UML als Modellierungssprache für den Programmentwurf 930 verwenden, sollte bei der Implementierung auch möglichst konsequent objektorientiert programmiert werden, d.h. im Wesentlichen keine globalen Daten und keine globalen Funktionen. Bei größeren Projekten kann es sehr sinnvoll sein, in verschiedenen Programmteilen unterschiedliche Programmiermethoden zu verwenden. In dem Falle wird man die Verwendung von UML auf die objektorientierten Programmteile beschränken. %'& (*),+.- /0+.1 &2/0& 345/76.8(895:<;*8=)& IKJMLNJPORQSJ TVUNW,XYUNJ0JMOAXZUW > +0)2+.:,?? "!$# ?)- 8(@86.8=/0+7;A/0B CD0EFD.G,HH Bild 12.1: Klassisches Phasenmodell der Softwareentwicklung 12.1.1 Modellierung von Klassen Der angegebene C++-Text deklariert die Schnittstelle einer Klasse Stack und das Bild 12.2 enthält vier UML-Modelle dieser Klasse mit verschiedenen Abstraktionsgraden. Das rechte Modell hat den höchsten Abstraktionsgrad, es besteht aus nur einem Rechteck mit dem Namen der Klasse. class Stack { int *Data; int n; int size; void Copy (const Stack& s); public: Stack (int siz=10); ~Stack (); Stack (const Stack& s); Stack& operator= (const Stack& s); void Push (int x); int Pop (); int count () const; }; Das linke Modell enthält die meisten Details. Im oberen Teil steht der Name der Klasse, im mittleren Teil die Datendefinitionen und im unteren Teil stehen die Deklarationen der Elementfunktionen. Vor jeder Deklaration ist ein Symbol angegeben, das ihre Zugriffsart definiert: + für public, - für private und # für protected. 12.1 931 Klassendiagramme: Modellierung der Architektur Die Syntax der Deklarationen unterscheidet sich etwas von der C++-Syntax, sie soll ja im Prinzip sprachunabhängig sein. Die Syntax der Deklarationen, die formal exakt in der UML-Beschreibung festgelegt ist, soll hier nicht weiter erörtert werden, sie ist für jeden C++-Programmierer leicht lesbar und bei der Erstellung von UMLModellen hilft dann das entsprechende Entwicklungswerkzeug. Beim Erstellen von Klassendiagrammen ohne die Verwendung von Werkzeugen spricht natürlich nichts dagegen, die C++-Syntax zu verwenden. Das zweite Modell von links verzichtet auf die Angaben der Signaturen bei den Deklarationen der Elementfunktionen und beim dritten Modell von links wird auf die Datendeklarationen verzichtet. ! "# %$'&#)( * + # " ! -,/.10"&## + # " ! " "%$'& " +32 " & +54 6 7 8 !&#%( * + -9 9, ! - %$:&# %$ +54 - &# + 6 &# ; < = & + & + & +2 & +4#6 7 & + - 9 9, & +4 & + 6 & " & + & + & +2 # & +4#6 7 & + - -9 9, & +4 & + 6 & Bild 12.2: UML-Modelle einer Klasse Stack mit unterschiedlichen Detaillierungsgraden Abstrakte Klassen, abstrakte Methoden Der folgende C++-Text enthält die Klassenschnittstelle einer abstrakten Klasse mit rein virtuellen Elementfunktionen und einem virtuellen Destruktor ; Konstruktoren und Zuweisungsoperator machen – in der öffentlichen Schnittstelle – keinen Sinn. Wenn eine abstrakte Klasse eigene Heapdaten verwaltet, sollten entsprechende Konstruktoren im protected-Bereich zur Verfügung gestellt werden und evtl. ebenfalls im protected- Bereich ein Zuweisungsoperator. class AbsStack { public: virtual AbsStack(); virtual void Push(int x) = 0; virtual int Pop() = 0; virtual bool isEmpty() = 0; }; Das Bild 12.3 zeigt UML-Diagramme für diese Klasse mit verschiedenen Abstraktionsgraden. Eine abstrakte Klasse wird in der UML-Darstellung durch einen kursiv 932 geschriebenen Klassennamen dargestellt und die rein virtuellen (abstrakten) Elementfunktionen werden ebenfalls durch kursiv geschriebene Namen und Signaturen wiedergegeben. @ ACB(DE FGIHJ KMLON !" # P>$QS&(RCT'UW)V X% *Y+Z [",-\ ]^ Y<_a`Z b % .0/21354 6879 :<; .>=1?:<; 4 6879 :<; cdefg hij kmCl2(nopCqO2r M- > ktsu^Mvow5 x yz{qOr |~}M |0M I 82 ¡¢a£ ½% ¤m¾C¥2¿(¦§À ¨CÁÂ2©Oª à ÄÅÆMÇ-È É0 ¤tÊ«¬^ËÌM­Í§Î®5Ï ¯ °±²©Oª ³~´µM¶· ¸ ³0¹µº»<¼I· ¸ Ð8ÑÒÓ2Ô ÕÖ× Bild 12.3: UML-Modelle einer abstrakten Klasse mit unterschiedlichen Detaillierungsgraden 12.1.2 Modellierung von Beziehungen zwischen Klassen Komposition, Aggregation und Assoziation Grob gesehen ist zwischen benutzt-Beziehungen (hat ein) und erbt-Beziehungen (ist ein) zu unterscheiden; hier geht es zunächst um die erstgenannten. Der angegebene C++-Text und das Bild 12.4 stellen Komposition und Aggregation als zwei Varianten dieser Art von Beziehung dar. Komposition Aggregation class Stack { ... }; class Stack { ... }; class Application { Stack s; Stack svec[5]; public: void main(); }; class Application { Stack* ps; Stack* psvec1[20]; Stack (*psvec2)[20]; public: Application(); ~Application(); void main(); }; Komposition steht für eine stärkere Bindung, im C++-Code existiert ein konkretes Objekt der benutzten Klasse, im Diagramm wird dafür die ausgefüllte Raute verwendet. Aggregation steht für eine schwächere Bindung, im C++-Code wird das Objekt über einen Zeiger oder über eine Referenz benutzt, im Diagramm wird dafür die 12.1 933 Klassendiagramme: Modellierung der Architektur 3 3 5 3 (4- )# 3 021 1 3 3 0 3 "!$#&%' )# ( (+* , )# (.- /# 687:9<;&7&=> ?@> 7:A BDCCFEHGCI ?$> 7:A Bild 12.4: Die benutzt-Beziehungen Komposition und Aggregation leere Raute verwendet. Die Namen an den Verbindungen bezeichnen das benutzte Objekt bzw. den Zeiger oder die Referenz, über die das Objekt verwendet wird. Die Zahlen an den Verbindungen definieren die sog. Multiplizität. Im linken Teilbild benutzt ein Application-Objekt ein Stack-Objekt s und fünf Stack-Objekte in Form des Vektors svec. Entsprechend sind die Zahlen an den Verbindungen des rechten Teilbildes zu interpretieren, wobei 0..1 bedeutet, dass der Zeiger mit einem entsprechenden Objekt verbunden sein kann oder nicht. Als Angaben für Multiplizitäten werden z.B. verwendet: • 1 für genau ein; • n für n; • * für viele; • 0..1 für null oder ein; • n..m für n bis m. Wenn es darum geht, vorhandenen C++-Code durch Klassendiagramme zu beschreiben, sind die Beziehungen im Allgemeinen klar definiert als Komposition oder als Aggregation. Anders ist die Situation, wenn – wie bei der realen Softwareentwicklung nach einem Phasenmodell – die Softwarearchitektur zuerst durch Klassendiagramme modelliert und das Modell in einer späteren Phase implementiert wird. Dann kommt es vor, dass die Art der Beziehung im Detail noch nicht definiert werden kann. In diesem Falle verwendet man einen entsprechenden Pfeil ohne Raute oder – falls auch die Richtung noch offen ist – eine Verbindung ohne Pfeil. Diese Art der Beziehung 934 wird Assoziation genannt und kann als Oberbegriff zu Komposition und Aggregation angesehen werden. Selbstbezüge In der Praxis werden häufig rekursive Datenstrukturen verwendet, wie z.B. Listen- oder Baumknoten. Der angegebene C++-Text und das Bild 12.5 zeigen eine entsprechende Klasse, die einen Listenknoten mit den entsprechenden Zugriffsoperationen darstellt und ihre Verwendung zur Realisierung einer Stack-Klasse. Selbstverständlich kann eine Klasse sich nicht direkt selbst benutzen, sondern nur indirekt über einen Zeiger oder über eine Referenz! 7 - /. / . / / + &, 536 ' # &, & , 0 0 ,&, + ,&1 ,,23$ + &,&14 89 9 7 - 53 6 - , 89 9 7 7 "! # $ &%' $ ()* Bild 12.5: Benutzung einer Klasse mit Selbstbezug class Stack { class Node { Node* next; int data; public: int getdata(); void setdata(int x); Node* getnext(); void setnext(Node* p); }; Node *first; public : Stack (); ~Stack (); void Push (int x); int Pop(); bool isEmpty(); }; 12.1 Klassendiagramme: Modellierung der Architektur 935 Vererbung (Typerweiterung, Subtyping, Spezialisierung) Der angegebene C++-Text und das Bild 12.6 demonstrieren die Modellierung einer Vererbungsbeziehung; der Pfeil geht von der Unterklasse aus und zeigt zur Oberklasse, d.h. er zeigt in Richtung der Generalisierung. Spezielle Konstrukte der Sprache C++, wie z.B. private Vererbung, werden in UML nicht modelliert. 0 1 & 2 0 3 & 0 054 "6$ *7 " 6 81:9 " ; 3 =< 7& " &6 >81& 7? + A@ /&B( C5 D>9 " ; "$ " D7 " >8E& >8 A@#"6$ + " /(&D+ !#"$ % & (') $ *+, " " (./ - - %, "("- Bild 12.6: Die Vererbungsbeziehung zur Erweiterung einer Klasse Das Symbol # vor einer Deklaration bedeutet, dass das entsprechende Element die Zugriffsart protected hat. class Stack { protected: int *Data; int n; int size; void Copy (const Stack& s); public: Stack (int siz=10); ~Stack (); Stack (const Stack& s); Stack& operator= (const Stack& s); void Push (int x); int Pop (); int count () const; }; class DirStack : public Stack { public: DirStack(int size=20); int Top() const; bool isEmpty() const; bool isFull() const; }; 936 Abstrakte Klassen als Schnittstellenklassen Bild 12.7 zeigt ein bei der Entwicklung objektorientierter Software häufig verwendetes Muster, die Verwendung einer abstrakten Klasse als Schnittstelle zur Anwendung, der angegebene C++Text skizziert den entsprechenden Code. Die Vorteile dieses Entwurfsmusters sind vielfältig, z.B.: • Die Details der Implementierung werden konsequent versteckt. • Die Implementierungen – in Form abgeleiteter konkreter Klassen – sind zur Laufzeit austauschbar. • Partielle und inhomogene Zuweisungen werden syntaktisch ausgeschlossen, siehe dazu Abschnitte 9.4.3. \G]]^ _ `)abc_ d(e f&g \G]h]^ _ `)abc_ d#ehicj f \G]]h^ _ `)ahb_ d(eicjk!\G]h]^ _ `)abc_ d#e fml a(_ eiLjk!nd(_ o r pq q r s ]t +-,/.01 23)4 uO6vx7wh8y{9z(: | ;& }<~ = >#? U 5 @<!~#A!B# h= C 5&DEGF8H? IJK):L@ MON PQR SUTV W#X MZY)P[W#X R SUTV W#X !"# $ &% "(' )" *** Bild 12.7: Abstrakte Klasse als Schnittstelle zur Anwendung Die Verwendung der Klasse Stack – oder allgemein die Verwendung der von der abstrakten Klasse abgeleiteten konkreten Klassen – erfolgt über einen Zeiger vom 12.1 937 Klassendiagramme: Modellierung der Architektur Typ Zeiger auf abstrakte Klasse (Basisklassenzeiger), der dann mit den Zeigern vom Typ Zeiger auf abgeleitete konkrete Klasse kompatibel ist. D.h. zur Laufzeit des Programmes kann der Basisklassenzeiger an Objekte verschiedenen Typs gebunden werden. C++-Text 12.1: Abstrakte Klasse als Schnittstelle class AbsStack { public : virtual ~AbsStack virtual void Push virtual int Pop virtual int count }; () {} (int x) = 0; () = 0; () const = 0; class Stack : public AbsStack { int *Data; int n; int size; void Copy (const Stack& s); public : 12.1.3 (./bsp12/kurzecpptexte/relats4.sht) virtual virtual virtual virtual . . . ~Stack (); void Push (int x); int Pop (); int count () const ; }; class Application { AbsStack* ps; public : Application(); ~Application(); void main(); }; Verwendung von Klassenschablonen Bild 12.8 zeigt die Verwendung einer Klassenschablone (Klassen-Template) in einem UML-Diagramm, der angegebene C++-Text skizziert den zugehörigen Code. Die Klasse Application in dem Diagramm benutzt nicht die Schablone Stack, sondern eine Klasse Stack<int>, die aus der Schablone generiert wird. Das gestrichelt dargestellte Rechteck mit der Bezeichnung T in der Klassenschablone Stack bezeichnet den Typparameter. Der gestrichelte Pfeil von der Klasse Stack<int> zu der Schablone bedeutet in diesem Zusammenhang eine Klasse aus ” einer Schablone erzeugen“. Die Verwendung von Klassenschablonen beinhaltet einen Mechanismus zum Generieren von Code, mit wenig Programmieraufwand kann viel Code erzeugt werden, der dann statisch typisiert und damit sicher ist und der darüber hinaus sehr laufzeiteffizient sein kann, siehe die STL (Standard Template Library). Auf der anderen Seite sollte aber auch beachtet werden, dass der Einsatz von Klassenschablonen auch ein großes Codevolumen erzeugen kann. Der Sprachstandard von C++stellt Konstrukte zur Spezialisierung von Schablonen zur Verfügung, die im Allgemeinen erfolgreich eingesetzt werden können, um das Codevolumen zu reduzieren; sie werden im Abschnitt 9.7 beschrieben.