141
1 Programmieren II Stand: 18.01.2015 Hinweis: Die Literaturhinweise für die Kapitel 1 3 entnehmen Sie bitte aus Programmieren I

Programmieren II - et-inf.fho-emden.dewenzel/prog2/data/prog2_vorles_2015_01_18.pdf · 6 Ein weiteres klassisches Beispiel für die Anwendung der Rekursion ist die Umwandlung von

  • Upload
    others

  • View
    10

  • Download
    0

Embed Size (px)

Citation preview

1

Programmieren II

Stand: 18.01.2015

Hinweis: Die Literaturhinweise für die Kapitel 1 – 3 entnehmen Sie

bitte aus Programmieren I

2

1 Unterprogrammtechnik (Fortsetzung/Wiederholung) 4

1.1 Rekursion ( Zusatzinformation ) 4 1.1.1 grundlegende Eigenschaften eines Stacks 4 1.1.2 Vorgänge bei Rekursion 4 1.1.3 Die Türme von Hanoi 7

2 ZEIGER ODER POINTER (ZUSATZLITERATUR) 9

2.1 allgemeine Grundlagen 9

2.2 Deklaration von Zeigern 10

2.3 Zeigeroperatoren 10

2.4 Funktionen zur dynamischen Speicherverwaltung in C (Auswahl) 12 2.4.1 Die Funktion "calloc" (Reservierung von Speicher, Zusatzinformation) 12 2.4.2 Die Funktion "malloc" (Reservierung von Speicher) 12 2.4.3 Die Funktion free (Freigabe von Speicher) 13

2.5 Zeiger auf Strukturen 13

2.6 ein erstes Zeigerbeispiel 13

2.7 Parameterübergabe an die "main - Funktion" 14

3 DATEIARBEIT IN C (ZUSATZLITERATUR) 16

3.1 Die Funktion "fopen" zum Öffnen einer Datei 16

3.2 Die Funktion "fread" zum Lesen einer Datei 17

3.3 Die Funktion "fwrite" zum Schreiben in eine Datei 18

3.4 Die Funktion "fclose" zum Schließen einer Datei 18

3.5 Die Funktion "fseek" zur Positionierung des Filepointers 19

3.6 Die Funktion "ftell" (Pointerposition) 19

Wichtige Hinweise: 23

4 URSACHEN OBJEKTORIENTIERTER ENTWURFSMETHODEN (ZUSATZINFORMATION, ZUSATZLITERATUR) 24

4.1 Geschichte der Softwareabstraktion (Zusatzinformation) 24

4.2 Aspekte der Softwarequalität 27

4.3 Aspekte der Modularität 29 4.3.1 Kriterien für den Modulentwurf 29 4.3.2 Prinzipien, um Modularität zu erreichen 31 4.3.3 Erstellung von Modulstrukturen 33 4.3.4 vorläufige Kurzzusammenfassung 37

4.4 funktionaler oder datenorientierter Entwurf 37

3

5 GRUNDBEGRIFFE DER OOP (ZUSATZLITERATUR) 39

6 DER OBJEKTORIENTIERTE ENTWURFSPROZEß (ZUSATZLITERATUR) 47

6.1 Die Bestimmung von Klassen 47

6.2 Die Bestimmung von Operationen / Verantwortlichkeiten 56

6.3 Bestimmung von Wechselbeziehungen (Schnittstellenbeschreibung) 60

6.4 Vorläufiges Design für die Fernsehgerätesimmulation 63

7 ENTWICKLUNG UND EINORDNUNG VON C++ (ZUSATZLITERATUR) 65

7.1 Ein -/ Ausgabe in C++ 67 7.1.1 Die Standardausgabe unter C++ 67 7.1.2 Die Standardeingabe unter C++ 68

8 KLASSEN IN C++ (ZUSATZLITERATUR, ZUSATZINFORMATION) 73

8.1 Grundbegriffe (Wiederholung) 73

8.2 Syntax in C++ 74 8.2.1 Daten in einer Klasse 74 8.2.2 Methoden einer Klasse 74 8.2.3 Objekte von Klassen 75

8.3 Konstruktoren und Destruktoren 76 8.3.1 Konstruktoren 76 8.3.2 Destruktoren 85

8.4 Der Referenztyp in C++ 90 8.4.1 prinzipielle Methoden der Parameterübergabe 90 8.4.2 Besonderheiten der Referenzmethode 94 8.4.3 Referenzen und Objekte 97

8.5 Freundfunktionen und Freundklassen 102 8.5.1 Freundfunktionen 102 8.5.2 Freundklassen 106

8.6 Operatorüberlagerung (Zusatzinformation, kein Bestandteil in Prog. II) 108 8.6.1 einfache Operatorüberlagerung 108 8.6.2 Überladen von Operatoren mit unterschiedlichen Bedeutungen 113 8.6.3 Operatorüberlagerung zur Behandlung von Objekten 114 8.6.4 Überlagerung des Increment- bzw. Dekrementoperators 119 8.6.5 klassengebundene Operatorüberlagerung (Zusatzinformation) 121 8.6.6 Typumwandlung mit Operatorfunktionen (Zusatzinformation) 124 8.6.7 Überlagerung des Funktionsaufrufoperators () (Zusatzinformation) 126 8.6.8 Überlagerung des Operators ' ->' (Überblick, Zusatzinformation) 126 8.6.9 Die Überlagerung des <<-Operators (Zusatzinformation) 127 8.6.10 Standardanweisungen auf Objekte (Zusatzinformation) 131

8.7 Der „this“ - Zeiger in C++ (Zusatzinformation) 134

8.8 Zusammenfassung zum Kapitel „Klassen“ 139

4

1 Unterprogrammtechnik (Fortsetzung und Wiederholung)

1.1 Rekursion ( Zusatzinformation )

In der Informatik gibt es zahlreiche Algorithmen, die auf einem Rekursionsverfahren aufbauen. Unter einer Rekursion versteht man programmtechnisch ein Unterprogramm, das sich selbst aufruft. Um Rekursionen durchführen zu können, ist ein sogenannter - Stapelspeicher oder - Kellerspeicher oder - Stack erforderlich. Diese Form der Organisation eines Speichers ist im übrigen immer notwendig, sofern man mit Unterprogrammen arbeitet.

1.1.1 grundlegende Eigenschaften eines Stacks

Stack-Speicher sind meist nach dem LIFO-Prinzip (Last In First Out) organisiert. Wenn man z.B. von einem Programmodul ein weiteres Unterprogramm aufruft werden u.a. folgende Aktionen ausgelöst: - Rettung (merken) der Adresse des nächsten Befehls im im aufrufenden Programm (hier wird nach dem Arbeiten des UP's weitergemacht) - Rettung der aktuellen Variablen und ihrer Werte - Rettung aktueller Adressen (ganz allgemein und zur Vereinfachung) - Rettung von kompletten Anweisungen, die noch nicht ausgeführt wurden (auch eine Vereinfachung), also Rettung aller Charakteristika!

1.1.2 Vorgänge bei Rekursion

Syntax (allgemeine Programmform): z.B.: int rekurs() { ... rekurs(); /* eigener Aufruf */ ... } Die Aktionen und die Abarbeitung soll an einem Beispiel erfolgen: Hier ein Beispiel einfügen

5

Beispiel zur einfachen Veranschaulichung von Vorgängen bei Rekursion:

6

Ein weiteres klassisches Beispiel für die Anwendung der Rekursion ist die Umwandlung von Zahlen in die zugehörige Bitfolge. Klassisch deshalb, weil die Abspeicherung der einzelnen Bitwertigkeiten bei echter Rekursion (keine Benutzung von Hilfsgrößen) so erfolgt, daß nur noch der Stack korrekt wieder abgebaut werden muß. Hier nun zunächst ein technisch orientierter Algorithmus. Eingabe der dezimalen Zahl Zahl > = nein Zahlenbasis ? ja Restdivision der Zahl durch Zahlenbasis Division der Zahl durch Zahlenbasis höchswertigstes Bit ist Zahl selbst Beispiel: Bitfolge für die Ziffer 5 Wert / Aktion Wertigleit (Bit) Stack

5 % 2 (mod-Div.), danach: 5 / 2 (nächster Wert)

20 (Variable "rest") printf("%1d",rest);

Ausgabe: 1

2 % 2 (mod-Div.), danach: 2 / 2 (nächster Wert)

21 (Variable "rest") printf("%1d",rest);

Ausgabe: 0 (dies wird beim Stack- abbau zuerst ausgeg.)

zahl =1 , (damit zahl<2) letzter Wert ist zahl selbst

22 (Variable "zahl") else

printf("%1d",zahl); Ausgabe: 1

keine Aktion mehr

7

1.1.3 Die Türme von Hanoi

Spiel erläutern.. (maximal 64 Scheiben ) A B C Algorithmus ganz allgemein: Das Umsetzen der Scheiben geschieht wie folgt: - n-1 Scheiben werden von A nach B transportiert - Scheibe n direkt von A nach C transportieren - n-1 Scheiben werden von B nach C transportiert Die Scheiben A, B, und C können dabei jeweils als Zwischenspeicher dienen. Programmtechnisch gesehen heißt das: Rekursionsschachtelung. Wenn man jeweils in 1s eine Scheibe transportiert und dabei keine Fehler macht, würde man dieses Problem für 64 Scheiben nach ca. 620.000 Jahren gelöst haben. Programmbeispiel: Folie /Skript (Beispiel 12)

8

/* beispiel 12 (auch als Quelltext in Prog. I): demonstriert rekursionsbeispiel. tuerme von hanoi */ #include <stdio.h> #include <stdlib.h> int n_start; void hanoi (int,char,char,char); /* prototyp */ void hanoi (int n,char apla,char zielpla,char zwischpla) { if(n==1) /* rekursion dann beenden */ printf("\nscheibe %2d von %c nach %c",n,apla,zielpla); else { hanoi(n-1,apla,zwischpla,zielpla); /* bringe n-1 scheiben von A nach B */ if(n==n_start) printf("\n\nscheibe %2d direkt von %c nach %c\n",n,apla,zielpla); /* bringe scheibe n direkt von A nach C */ else printf("\nscheibe %2d von %c nach %c",n,apla,zielpla); /* bringe scheibe n von A nach C */ hanoi(n-1,zwischpla,zielpla,apla); /* bringe n-1 scheiben von B nach C */ } } int main() { printf("\ntuerme von hanoi"); printf("\n\nanzahl der scheiben: "); scanf("%d",&n_start); hanoi(n_start,'A','C','B'); printf("\n\nfertig"); return 0; }

9

2 Zeiger oder Pointer (Zusatzliteratur)

2.1 allgemeine Grundlagen

Bei der Deklaration von Variablen wird diesen ein Speicherplatz im Speicherraum des Rechners zugewiesen. Man unterscheidet prinzipiell zwei Zugriffsmethoden auf derartige Speicherzellen: direkter Zugriff indirekter Zugriff Direkter Zugriff Die Wertzuweisung an eine Variable bewirkt die Veränderung des Speicherplatzes, d. h. der Inhalt dieses Platzes wird auf direktem Wege geschrieben. Gleiches geschieht beim Lesen einer Variable, die entsprechende Speicherzelle(n) wird direkt ausgewertet. Indirekter Zugriff Zeiger oder Pointer verweisen oder zeigen auf eine Speicherstelle, d. h. der Wert des Zeigers ist eine Adresse der Speicherzelle und nicht etwa deren Inhalt. Um eine Wertebeschreibung durchführen zu können, erfolgt ein indirekter Zugriff auf die Zelle oder den entsprechenden Platz. Eine Zuweisung an eine Zeigervariable bewirkt die Veränderung der Adresse auf die danach gezeigt wird, sie bewirkt aber keine Veränderung des Inhaltes. Zwei wesentliche Vorteile beim Umgang mit Zeigern sind zu verzeichnen: dynamische Speicherverwaltung Handhabung abstrakter (verketteter) Datenstrukturen Während bei einer Variablendeklaration ein Speicherplatz statisch zu gewiesen wird, der, auch wenn er nicht mehr benötigt wird, fest belegt ist, erfolgt bei der dynamischen Speicherverwaltung die Freigabe und Reservierung von Speichern während der Laufzeit. Damit sind sehr speicherintensive Programme realisierbar. Zum Beispiel kann ein Speicherraum, der nicht mehr benutzt wird, wieder für andere Operationen verfügbar gemacht werden. Der Umgang mit sehr komplexen bzw. abstrakten Datenstrukturen wird durch den Einsatz von Zeigern ebenfalls stark vereinfacht bzw. überhaupt erst möglich. Dieser Punkt der Zeigeranwendung soll aber im Rahmen dieser Veranstaltung nur im Überblick behandelt werden.

10

Zeiger benötigen im Gegensatz zu Variablen nur den Speicherplatz, um die physische Adresse zu speichern. bisherige Zusammenfassung (speziell für die Sprache C):

Zeiger können auf alle C-typen zeigen.

Zeiger sind für jeden Datentyp getrennt zu deklarieren.

dynamische Speicherverwaltung wird durch die Einführung von Zeigern erst möglich.

Zeiger spielen in C eine zentrale Rolle. Fast alle Standardfunktionen und Bibliotheken sind auf die Verwendung von Zeigern ausgerichtet.

Array's werden in C in Zeiger umgewandelt (Auswirkungen auf scanf-Funktion)

2.2 Deklaration von Zeigern

Syntax: <typ> *<bezeichner>; Beispiele: char *c; int *i; struct adresse *wohnung;

2.3 Zeigeroperatoren

Adreßoperator: & <zeigervariable> = &<variable>; Indirektoperator: * *<zeigervariable>=10; Beispiele: int x,y,*pint; x=10; pint = &x; /* adresse von x in zeiger speichern */ y= *pint; /* y=x */ *pint=0; /* x=0 */ *pint=*pint+2; /* x=x+2 */

11

erlaubte Operationen / Regeln: 1. arithmetische Operationen nur zwischen Zeigern gleichen Typs. 2. mögliche Operationen (ANSI-Standard - C ) sind: a) Vergleich (==, !=, ...) b) Arithmetik zwischen Zeigern c) Arithmetik von Zeigern und Integerwerten 3. grundsätzlich erfolgt die Arithmetik in Speichereinheiten des Typs, für den der Zeiger definiert wurde. Beispiel: int i, *pint; char c, *pchar; .... .... pint=&i; pchar=&c; pchar++; /* adresse von pchar + 1 byte */ pint++; /* adresse von pint + 4 byte, etc... */ 4. Zuweisung des Wertes NULL. Dieser Wert ist in C vordefiniert. Sofern dieser Wert einem Zeiger zugewiesen wird, zeigt er auf keine bestimmte Adresse mehr und spiegelt somit auch keine bestimmten Speicherauszug mehr wieder. Trotzdem ist der Zeigerwert "NULL" ein bestimmter Wert (also nicht ein unbestimmter, wie nach dessen Deklaration) und kann ausgewertet werden. Große Bedeutung hat dieser Wert einerseits bei der Verarbeitung komplexerer Datenstrukturen, andererseits geben zahlreiche Standardfunktionen diesen Wert bei fehlerhaften Ausführungen zurück. 5. vor dem indirekten Zugriff (*zeiger) muß unbedingt eine Reservierung von Speicher erfolgen, da die Adresse des Zeigers nach seiner Deklaration unbestimmt ist. Beispiel: int i, *pint; *pint =2; /* schreibt irgendwo hin memory core dump */ pint = &i; *pint=2; /* ok */ Um nun Speicherplatz dynamisch verwalten zu können, gibt es wiederum Standardfunktionen.

12

2.4 Funktionen zur dynamischen Speicherverwaltung in C (Auswahl)

2.4.1 Die Funktion "calloc" (Reservierung von Speicher, Zusatzinformation)

Syntax: void * calloc (unsigned n, unsigned size) void * : typenloser Zeiger als Rückgabewert (enthält die Startadresse des reservierten Speichers) (sofern Typzuweisung erfolgen soll, Anwendung des CAST-Op.) unsigned n: wieviel Speicherplätze entsprechend der angegebenen SIZE. unsig. size: Datentyp: (sizeof(int)) (sizeof(char)) (sizeof(struct adresse)) ...etc. Beispiele: int *pint; char *pchar; pint =(int *) calloc (10, sizeof(int)); /* 10 integerwerte */ pchar = (char *) calloc(20, sizeof(char)); /* 20 Bytes */

2.4.2 Die Funktion "malloc" (Reservierung von Speicher)

Syntax: void * malloc (unsigned size); void * : Zeiger als Rückgabewert (enthält die Startadresse des reservierten Speichers) (sofern Typzuweisung erfolgen soll, Anwendung des CAST-Op.) unsigned size: wieviel Bytes sollen reserviert werden Beispiel: pint = (int *) malloc(100); /* 100 Bytes, 25 Integerwerte */ besser: pint = (int *) malloc(25 * sizeof(int));

13

2.4.3 Die Funktion free (Freigabe von Speicher)

Syntax: free(<zeigervariable>); Beispiel: free(pint);

2.5 Zeiger auf Strukturen

Verwendung findet hier der sogenannte Pfeiloperator -> Bsp.: struct adresse { int plz; char strasse[20]; } *haus; haus = (struct adresse *) malloc(sizeof(struct adresse)); haus -> plz = 10000;

2.6 ein erstes Zeigerbeispiel

/* beispiel 13 (aus Prog1): demonstriert eine erstes beispiel zur handhabung von zeigern */ #include <stdio.h> #include <stdlib.h> int i; /* normale variable */ int *p_int; /* zeigervariable. diese hat momentan noch einen unbestimmten wert. ein indirekter zugriff darf also noch nicht erfolgen */ struct zeigtest { int feld1[10000]; char feld2[20]; }; /* noch keine festlegung der instanz */

14

int main() { struct zeigtest *zeiger; /* vereinbarung der instanz als zeiger */ /* nachfolgende zugriffe sind undefiniert */ for(i=0;i<10000;i++) zeiger->feld1[i]=i; *p_int=2; /* wohin wird geschreiben??? */ /* jetzt korrekte speicherreservierung */ zeiger=(struct zeigtest *)malloc(sizeof (struct zeigtest)); p_int=(int *)malloc(sizeof(int)); for(i=0;i<10000;i++) /*jetzt alles i.o. */ zeiger->feld1[i]=i; *p_int=2; free (p_int); /* freigabe */ free (zeiger); /* dito */ return 0; }

2.7 Parameterübergabe an die "main - Funktion"

Von der Betriebssystemebene können beim Aufruf von Programmen gleich Parameter übergeben werden. (Texdteditor, etc...) Syntax in C: void main (int argc, char *argv[ ]) { ... } argc: Anzahl der Parameter argv[ ]: Adresse eines Feldes (Stringelement). Jedes Feldelement enthält die Startadresse eines Aufrufparameters Achtung!!!! Der Programmname selbst wird ebenfalls als Parameter gezählt!!!

15

Beispiele: Programmname sei: parameter.exe Parameter 1: test.dat Parameter 2: test.bak Aufrufbeipiele von BS-Ebene: parameter argc = 1 (nur Programmname) argv[0] = "parameter.exe" parameter test.dat argc = 2 (Name und 1. Parameter) argv[0] = "parameter.exe" argv[1] = "test.dat" parameter test.dat test.bak argc=3 usw... printf("\n%s ",argv[1] ); /* test.dat wird ausgegeben */ Über diese Möglichkeiten kann man dann auch auf die jeweiligen Daten zugreifen. Aufgabe: Ein Programm soll eine von der BS-Ebene eingegebene einstellige Zahl (1 Parameter) mit 3 multiplizieren und das Ergebnis ausgeben. Falls die Anzahl der Parameter unzu- lässig ist, soll eine Fehlermeldung ausgegeben werden. #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) /* entspricht: **argv */ { int i; if (argc!=2) /* falsche argumentenzahl */ { printf("\nfehler\n"); } else { i = *argv[1]-0x30; /* indirekter zugriff, ascii-->integer */ printf ("\n%d * 3 = %d\n",i,i*3); } return 0; }

16

3 Dateiarbeit in C (Zusatzliteratur)

Es gibt mehrere Standardfunktionsgruppen, die eine effektive Dateiarbeit in C zulassen. Für eine spezielle Funktionsgruppe zur Dateiarbeit existiert eine Standardstruktur, die alle wesentlichen Eigenschaften einer Datei enthält. Diese Struktur trägt den Namen: FILE /* groß geschrieben, da Standardstruktur */ Auf diese Struktur muß entsprechend der Deklaration in <stdlib.h> und <stdio.h> ein Zeiger vereinbart werden. Für unsere Belange ist der Aufbau der Struktur nicht relevant und soll hier nicht näher betrachtet werden.

3.1 Die Funktion "fopen" zum Öffnen einer Datei

Öffnen einer Datei bedeutet, daß der Filepointer, mit dem man auf den Dateiinhalt zugreift, auf den Dateianfang gesetzt wird. Syntax: FILE *<instanz>; <instanz> = fopen (<dateiname>, <dateityp>); <dateiname>: Direktwert (String) oder Zeiger auf String <dateityp>: "r" read "w" write (datei, falls vorhanden, wird gelöscht) "a" append(schreiben ab dateiende) "+" typ-verknüpfung "r+" /* lesen und schreiben */ "b" binary-mode "t" text-mode (standard)

17

Kombinationen über den '+'-Operator sind auf diese Weise möglich. Beispiele: .... FILE *fp; 1. fp=fopen("test.dat","rb"); /* nur lesen, binary */ 2. char name[20]; ... scanf("%s",name); fp=fopen(name,"rb+"); /* lesen und schreiben */ 3. fp=fopen("c:\\wenzel\\test.doc","ab"); 4. void main(int argc, char *argv[ ]) ... fp=fopen(argv[1],"rb"); /* 1. Parameter von BS-Ebene */ Die Funktion fopen liefert den Zeigerwert NULL zurück, wenn das Öffnen der Datei ein Fehler verursacht hat (z.B. Datei nicht vorhanden). Dieser Wert sollte stets abgefragt werden. Beispiel: FILE *fp; ... if ((fp=fopen("test.doc","rb"))==NULL) { printf("\nfehler beim oefnen"); return 0; }

3.2 Die Funktion "fread" zum Lesen einer Datei

Syntax: int fread( void *ptr, int size, int n, FILE *fp); void *ptr: Adresse der Variablen, wohin eingelesen wird int size: Datentyp (z.B. sizeof(char), sizeof(int) sizeof(struct adresse), etc... ) int n: wieviel Elemente der Größe size sollen auf einmal gelesen werden FILE *fp: von welcher Datei, die mittels "fopen" geöffnet wurde

18

"fread" liefert den Wert NULL zurück, wenn die Leseoperation nicht ausgeführt werden kann (z.B. am Dateiende angelangt). Beispiel: .... FILE *fp; char c; /* Variable, wohin gelesen werden soll */ void main(void) { if ((fp=fopen("test.doc","rb"))!=NULL) /* != NULL : alles in Ordnung */ { while((fread(&c,sizeof(char),1,fp))!=NULL) /* !=NULL: lesen o.k., byteweises lesen, da sizeof(char) und n=1 ist) */ printf("%c",c); /* byteweise ausgabe einer datei */ } }

3.3 Die Funktion "fwrite" zum Schreiben in eine Datei

Syntax: int fwrite(void *ptr, int size, int n, FILE *fp); gleiche Funktionen wie fread.

3.4 Die Funktion "fclose" zum Schließen einer Datei

Syntax: fclose(FILE *fp); Beispiel.: fclose (fp); /* falls fp Zeiger auf FILE war */

19

3.5 Die Funktion "fseek" zur Positionierung des Filepointers

Syntax: int fseek(FILE *fp, long int offset, int whence) FILE *fp: welcher Dateipointer soll gesetzt werden long int offset: gewünschte Position bezogen auf "int whence", d.h. relative Positionierung int whence: SEEK_SET (0) relativ zum Dateianfang SEEK_CUR (1) relativ zur aktuellen Position SEEK_END (2) relativ zum Dateiende Beispiele: FILE *fp; long offset = 100; ... /* Annah., daß Datei geöffnet ist */ fseek(fp,0,SEEK_SET); /* dateianfang */ fseek(fp,0,0); /* dito */ fseek(fp,offset,SEEK_SET); /* 100. Pos. bezogen auf dateianfang */ fseek(fp, - offset,SEEK_END); /* 100 Pos. Vor dateiende */ fseek(fp,-1,SEEK_CUR); /* eine Position bezogen auf die aktuelle zurück */

3.6 Die Funktion "ftell" (Pointerposition)

Syntax: long ftell(FILE *fp); Gibt die aktuelle Pointerposition aus. Beispiel: FILE *fp; long pos; pos=ftell(fp); /* aktuelle position */ fseek(fp,ftell(fp)-1,SEEK_SET); /* eine position zurück */

20

Aufgabe: Es soll die Länge einer Datei ermittelt werden. Variante 1: (durch byteweises lesen) Variante 2: (durch positionierung auf dateiende) /* schneller */ /* beispiel 19: demonstriert eine weiteres beispiel zur dateiarbeit, dateilaengenermittlung */ #include <stdio.h> #include <stdlib.h> FILE *fp; char c; /* jedes zeichen lesen */ long unsigned laenge=0; /* für dateilaenge mittels bytelesen */ long auchlaenge=0; /* mittels ftell(fp); */ int main() { if ((fp=fopen("datlaeng.c","rb"))!=NULL) { while((fread(&c,1,1,fp))!=NULL) laenge++; /* byteweise lesen */ printf("\ndateilaenge (mittels byteweises lesen %lu",laenge); fseek(fp,0,0); /* dateianfang */ fseek(fp,0,SEEK_END); /* dateiende*/ auchlaenge=ftell(fp); /* schneller */ printf("\ndateilaenge (mittels ftell %ld",auchlaenge); } return 0; }

21

Besonderheit von „fseek“ zum Umschalten (Lesen/Schreiben und umgekehrt) Beispiel:

22

Literaturhinweise für den Teil C++

/1/ R. Brock: Objektorientiertes Softwaredesign. Hansa- Verlag, 1990 (ISBN 3-446-16319-0) /2/ G. Booch: Objektorientierte Analyse und Design. Addison-Wesley, 1995 (ISBN 3-89319-673-0) /3/ Ulrich Breymann: C++ Einführung und professionelle

Programmierung. 768 Seiten, Hanser Verlag, ISBN 978-3-446-41023-7.

/4/ Ulla Kirch-Prinz, Peter-Prinz: C++ lernen und

professionell anwenden. 921 Seiten, MITP Verlag Bonn, ISBN 978-3826617645

/5/ Ulla Kirch- Prinz, Peter –Prinz: C++ Das Übungsbuch,

512 Seiten, Verlag wie eben, ISBN 978-3826617652 /6/ J. Wolf: C++ von A-Z. 1229 Seiten, Galileo Press, 2006,

ISBN-10: 3898428168 /7/ Bjarne Stroustrup: The C++ Programming Language. 2. Auflage, Addison-Wesley-Verlag, 1991

23

Wichtige Hinweise:

Das vorliegende Skript ist umfangreicher als der behandelte Stoff in der Vorlesung und im Praktikum. Kapitel (Abschnitte) oder Teile von Kapiteln (Abschnitten), die nicht in der Vorlesung behandelt werden, sind durch die Bemerkung "Zusatzinformation" gekennzeichnet. Diese dienen dazu, die Erkenntnisse abzurunden bzw. andere Sichtweisen zu betrachten.

Einige Teile des Skriptes werden in der Vorlesung nur verkürzt oder überhaupt nicht behandelt. Diese Kapitel oder Abschnitte werden durch die Bemerkung "Zusatzliteratur" gekennzeichnet und dienen dann im Zusammenhang mit der Vorlesung und der angegebenen Literatur zur effektiven Nacharbeitung. Stellenweise dienen diese auch nur der allgemeinen Information.

Die meisten Beispielprogramme sind im Skript vorhanden. Zusätzliche Beispiele drucken Sie bitte unter Visual C 2010 aus, da sie dort formatiert sind.

Dieses Skript ersetzt auf keinen Fall den Besuch der Vorlesung. Gleichzeitig ersetzt es nicht das Studium einschlägiger Literatur.

24

4 Ursachen objektorientierter Entwurfsmethoden (Zusatzinformation, Zusatzliteratur)

4.1 Geschichte der Softwareabstraktion (Zusatzinformation)

Ausgangspunkt war die (bereits erwähnte) Softwarekrise, die allerdings bis zu zum heutigen Tag nicht etwa beseitigt ist. Die immer größer werdende Komplexität, verbunden mit hoher Detailtiefe führte dazu, daß wichtige Faktoren wie Erweiterbarkeit und Wiederverwendung immer schwieriger zu bewältigen sind. So kam es, daß viele kleine Fehler (die viele Menschen in einem Softwaresystem mit einbringen) große Auswirkungen auf das System an sich nach sich zogen. Einen Ausweg bildet hierzu die Abstraktion. Abstraktion: - Schaffung gedanklicher Modelle, die einfacher sind als die komplexe Welt (Landkarten, Bilderkennung (egal wie groß oder gedreht...)-->GEHIRN tut dieses in nahezu perfekter Form) - Schlüssel zu immer besseren Softwaredesign Geschichte der Softwareabstraktion: 1. Ausführung von binären Instruktionen (bitweise Orientierung), Setzen von Schaltern (absolut maschinenabhängig) 2. Assemblersprachen: Menschen sollten sich nicht mehr darum kümmern, welche Schalter gesetzt werden müssen. Assembleranweisungen taten dies durch Befehlsnamen (höhere Abstraktionsebene). 3. Erzeugung von Makrobefehlen: Zusammenfassung von Befehlen unter 2. zu einer Befehlsfolge. Dieser Befehlsfolge wurde ein Name zugeordnet (Vereinfachung, höhere Abstraktion) 4. höhere Programmiersprachen: Menschen brauchten sich nicht mehr um die Rechnerarchitektur, Hardware, etc.. zu kümmern. Gleichfalls wurde der Befehlsumfang uninteressant. Jede Instruktion ruft eine Vielzahl von (maschinellen) Befehlsfolgen auf (z.B. Addition zweier Zahlen), ohne das man sich darum kümmern muß, wie das geschieht (schon sehr hohes Abstraktionsniveau).

25

5. Prozeduren: Enthalten Anweisungsfolgen (auf Basis höherer Programmiersprachen). Hier erfolgt der Aufruf mit einem neuen Namen. Welche Anweisungsfolge schließlich abgearbeitet wird, ist völlig uninteressant, die Abarbeitung bleibt verborgen. Einfache Beispiele: clrscr() gotoxy(int,int) Grafikanwendungen (Linien, Kreise)... es entstehen auf diese Weise immer mehr Bibliotheken. 6. abstrakte Datentypen: existieren erst seit relativ kurzer Zeit. Die Programmentwicklung erfolgt hier ohne Kenntnis der Datenrepräsentation. Die Details bleiben auch hier verborgen. Beispiel: Realisierung einer Menge, die aus unterschiedlichen Elementen besteht. Den Operationen, die auf diese Menge angewendet werden, ist es egal, ob diese Elemente in einem Feld, in einer Liste oder ... abgelegt sind. Das objektorientierte Design zerlegt nun ein System in seine abstrakten Bestandteile, wobei weitere Abstraktionsmechanismen (also eine weitere höhere Abstraktionsstufe) hinzugefügt werden. Die so genannten Objekte stehen für diese neue Entwicklungsstufe in der Softwareabstraktion. Was sind nun (oberflächlich betrachtet) Objekte ? herkömmliche Betrachtungsweise: - zunächst werden alle anstehenden Aufgaben - danach erfolgt die Zerlegung in Teilaufgaben - Funktionen und Daten werden bestimmt, wobei die Funktionen im Vordergrund stehen Das heißt: WIE realisiere ich etwas? objektorientierter Ansatz: - erste Frage hier: Nenne den Programmzweck, also WAS tut etwas?

26

- Ziel ist zunächst die Bestimmung der Objekte, die aus der Aufgabenstellung hervorgehen. - danach erfolgt die Bestimmung der auszuführenden Operationen - und erst danach erfolgt die Bestimmung der Verantwortlichkeiten und der Wechselbeziehungen zwischen den Objekten. Dadurch weiß jedes Objekt was es zu tun hat. Objekte: - verfügen über Informationen, die sie selbst betreffen. (Mensch kennt sein Auto, seinen Partner) - wissen, wie sie bestimmte Operationen ausführen müssen (wie fahre ich mein Auto, wie beseitige ich Streit mit meinem Partner..) - kennen aber nicht alle Systeminformationen (wie fahre ich ein anderes Auto, wie ist ein anderer Partner,...) - kapselt Informationen (Informationen und Operationen) und gibt nur ein wenig von seinem Wesen der Öffentlichkeit preis (Verbergen von Informationen) - heißt aber auch: Die anderen wissen nur so viel von diesem Objekt wie sie wissen müssen, um mit diesem umzugehen. Was bedeutet dies für den Softwarelebenszyklus? Test Anforderungs- spezifikation Implemen- Design tierung Herkömmlich

Test Anforderungs- spezifikation objektorientiertes Design dauert länger, trägt Implemen- aber dazu bei, das Software viel länger hält tierung und leichter erweiterbar ist. Damit erhöht sich Design die Softwarequalität.

objektorientiert

27

4.2 Aspekte der Softwarequalität

Softwaresysteme sind eine Menge von Mechanismen, um bestimmte Aktionen auf bestimmten Daten auszuführen. Software soll für lange Zeit nach der Fertigstellung nutzbar sein. Deshalb werden bestimmte Qualitätskriterien gestellt. Wesentliche Merkmale sind dabei: - Korrektheit - Robustheit - Erweiterungsfähigkeit - Wiederverwendbarkeit - Kompatibilität - Portabilität - Integrität Korrektheit: Korrektheit ist die Fähigkeit von Softwareprodukten, ihre Aufgabe exakt zu erfüllen. Robustheit: Robustheit ist die Fähigkeit von Softwareprodukten, auch unter außergewöhnlichen Bedingungen zu funktionieren. Erweiterungsfähigkeit: Bezeichnet die Leichtigkeit, mit der Softwareprodukte an Spezifikationsänderungen angepaßt werden können. Änderungen in kleinen Programmen sind meist problemlos zu realisieren. Oftmals erscheinen jedoch große Softwaresysteme wie gigantische aber eben zerbrechliche Konstruktionen. Nimmt man einen Stein heraus, bricht alles zusammen, wie ein Kartenhaus. Zwei wesentliche Prinzipien zur Verbesserung der Erweiterbarkeit sollen hier erwähnt werden: - Einfachheit des Entwurfes (einfache Architektur) - Dezentralisierung (je autonomer Module in einem Softwaresystem sind, desto einfacher sind die Änderungen, d.h. Änderungen betreffen nur oder wenige Module und lösen keine Kettenreaktion aus.)

28

Wiederverwendbarkeit: Eigenschaft von Software, ganz oder teilweise für neue Anwendungen wieder verwendet werden zu können. Es muß möglich sein, Gemeinsamkeiten von Softwareprodukten auszunutzen, um das Fahrrad nicht erneut und immer wieder erfinden zu müssen. Wiederverwendbarkeit beeinflußt alle anderen Aspekte der Softwarequalität. Wenn dieses Problem gelöst ist, muß schließlich weniger Software geschrieben werden, so daß also Korrektheit, nicht jedes mal neu erzielt werden muß. Kompatibilität: Maß der Leichtigkeit, mit anderen Softwareprodukten verbunden werden zu können (z.B. ist es schlecht, verschiedene Dateiformate zu verwenden). Portabilität: Maß der Leichtigkeit, mit der Softwareprodukte auf verschiedene Hard- und Softwareplattformen übertragen werden kann. Integrität: Maß der Fähigkeit, sich gegen unberechtigte Zugriffe und Veränderungen zu schützen. Die Qualitätsmerkmale - Erweiterbarkeit - Wiederverwendbarkeit - Kompatibilität und - Integrität stellen dabei die entscheidende Ursache für den objektorientierten Entwurfsansatz dar. Das Hauptziel des objektorientierten Entwurfs besteht darin, flexible Systemarchitekturen zu erzeugen. Software muß modularer gemacht werden, wobei neue Anforderungen an ein Modul gestellt werden.

29

4.3 Aspekte der Modularität

Unter modularer Entwicklung wurde bisher die Entwicklung von Programmen mit - Unterprogrammen - Funktionen oder - Prozeduren verstanden. Diese Technik kann jedoch nur dann zur Verwirklichung der oben definierten Ziele führen, wenn die Module - autonom - in sich geschlossen und - in robusten Architekturen organisiert sind. Das Unterprogramm, wie es aus der funktionsorientierten Softwareentwicklung bekannt ist, genügt diesen Ansprüchen jedoch nicht mehr, da die hierfür nötigen Techniken, wie z.B. das Vererben oder dynamisches Binden, nicht zur Verfügung stehen. Es ist also nötig, fortgeschrittene Modulformen einzuführen. Auf diese Weise soll Modularität im objektorientierten Sinne neu verstanden werden. Objektorientierter Entwurf ist dabei als Hilfsmittel zu sehen, daß es Entwicklern ermöglicht, Softwaresystem aus autonomen Modulen zu erzeugen."

4.3.1 Kriterien für den Modulentwurf

- modulare Zerlegbarkeit - modulare Kombinierbarkeit - modulare Verständlichkeit - modulare Stetigkeit - modulare Geschütztheit Zerlegbarkeit: - Zerlegung des Gesamtproblems in verschiedene Teilprobleme - verschiedene Entwickler bearbeiten das Gesamtproblem, d.h. jeder von ihnen muß "gleich" (von der Art und Weise her) programmieren. Kombinierbarkeit: " Eine Methode erfüllt das Kriterium der modularen Kombinierbarkeit, wenn sie Softwareelemente hervorbringt, die zur Herstellung neuer Systeme frei kombiniert werden können. Kombinierbarkeit befaßt sich im Gegensatz zur Zerlegbarkeit mit dem Prozeß des Zusammenfügens vorhandener Softwareelemente. Sie ist die unmittelbare Lösung des Wiederverwendbarkeitsproblems. Kombinierbarkeit und Zerlegbarkeit sind unabhängig voneinander, stehen jedoch bei den meisten Entwurfsmethoden im Widerspruch zueinander.

30

Beispiel: falscher Entwurf in bezug auf Kombinierbarkeit M1 M2 M3 M3 M4 M6 M5 Annahme: hier funktionieren (und nur für dieses Beispiel) alle Module korrekt. M1 M2 M3 M4 M5 M4 M6 - hier wird M5 über M2 aktiviert (anders also als vorher) - hier wird M4 über M3 aktiviert (auch anders) - M4 und M5 funktionieren nicht mehr, konnten also nicht frei kombiniert werden. Verständlichkeit: Dieses Kriterium ist erfüllt, wenn Module erzeugt werden, die für sich verständlich sind. Dies ist im Hinblick des Wartungsproblems von Bedeutung. Werden schon frühzeitig verständliche Module erzeugt, zahlt sich das bei der Implementierung und (leider sehr viel) später durch die wesentlich leichtere Wartung aus. Einige Kriterien hierfür sind:

31

- lesbarer Text (Einrücken, Namenswahl) - Kommentare - Programmköpfe Stetigkeit: Dieses Kriterium ist erfüllt, wenn kleine Änderungen der Spezifikation sich nicht als Änderung vieler Module auswirkt oder deren Beziehung zueinander verändert. Auf diese Art und Weise werden flexible Softwaresysteme geschaffen, --> Erweiterbarkeit steht im Vordergrund. Modulgeschütztheit: Dieses Kriterium ist erfüllt, wenn Module erzeugt werden, in denen die Auswirkung einer zur Laufzeit auftretenden Ausnahmesituation (Laufzeitfehler) auf dieses Modul (oder höchstens einige wenige Nachbarmodule) beschränkt bleiben.

4.3.2 Prinzipien, um Modularität zu erreichen

- wenige Schnittstellen - schmale Schnittstellen - explizite Schnittstellen - Geheimnisprinzip wenige Schnittstellen: Jedes Modul sollte möglichst wenig mit anderen Modulen kommunizieren. Wenn ein System aus n-Modulen besteht, dann sollte die Anzahl der intermodularen Verbindungen nahe des Minimums n-1 und weit unterhalb des Maximums n(n-1)/2 liegen. Damit wird die Architektur des Systems übersichtlich gehalten. Es sind mehrere Prinzipien bekannt, die dieses Ziel verfolgen. Verbreitet ist u.a. das so genannte Master-Slave-Prinzip (Bild nächste Seite). Hier gibt es einen Boss, mit dem jedes Modul ausschließlich kommunizieren kann. In objektorientierten Techniken gibt es liberalere Prinzipien über Nachbarmodule (Bild nächste Seite). Dieses Prinzip führt zu erstaunlich robusten Kommunikationsketten oder Ringstrukturen. schmale Schnittstellen:

Wenn zwei Module überhaupt kommunizieren, sollen nur wenige Informationen (Parameter, Rückgabewerte) ausgetauscht werden. Dieses Prinzip wird auch lose

Kopplung genannt. Es bezieht sich vor allen Dingen auf die Größe der intermodularen Verbindungen. Damit wird die Ausbreitung von Fehlern verhindert.

32

Möglichkeiten zur Realisierung "weniger" Schnittstellen: a) Master-Slave-Beziehung: M1 M4 BOSS M2 M5 M3 n Module: n-1 Schnittstellen b) Nachbarschaftsprinzip: M1 M2 M3 M4 M5 M6 n Module: n Schnittstellen (liberaler als Master-Slave-Prinzip)

33

Das Prinzip schmaler Schnittstellen verbietet: - Verwendung globaler Variablen und - riesige Parameterlisten explizite Schnittstellen: Wenn zwei Module M1 und M2 kommunizieren, dann muß das aus dem Text von M1 und M2 oder beiden hervorgehen. Hinter diesem Prinzip stehen die Kriterien Zerlegbarkeit, Kombinierbarkeit und Verständlichkeit. Geheimnisprinzip: Jede Information über ein Modul sollte modulintern sein, wenn sie nicht ausdrücklich als öffentlich erklärt wird. Das bedeutet, daß jedes Modul durch die Beschreibung seiner Schnittstelle, nicht aber des gesamten Modulinneren, mit der Umwelt in Verbindung steht. Die Schnittstelle sollte dabei die Beschreibung der Funktion sein. Gleichzeitig darf auf Daten des Moduls nur das Modul selbst zugreifen. Die Schnittstelle zur Umwelt sollte wiederum schmal sein. öffentlich Geheim Modul Wie kann man nun zu effizienten Modulstrukturen gelangen??

4.3.3 Erstellung von Modulstrukturen

Die Erläuterungen sollen anhand eines Beispiels erfolgen. Als Grundlage soll ein Tabellensuchprogramm (in allgemeiner Form) dienen /1/. Um wieder verwendbare Module zu erhalten sind erneut fünf Probleme zu lösen:

34

- Typ-Variation - Datenstruktur- und Algorithmen-Variation - Schaffung zusammenhängender Routinen (Pakete) - Darstellungsunabhängigkeit - Aufdecken von Gemeinsamkeiten zwischen Teilbaugruppen a) Typ-Variation: Die Module sollten auf möglichst viele verschiedene Datentypen anwendbar sein. So sollte es z.B. in der Tabellensuche egal sein, welchen Typ die Elemente der Tabelle haben. Dies erfüllt das Kriterium der Wiederverwendbarkeit (Algorithmus kann auch in anderen Projekten angewendet werden). b) Datenstruktur- und Algorithmen-Variation: Ebenso wie die Datentypen müssen auch die Datenstrukturen und Algorithmen allgemein gehalten werden. Suchalgorithmen müssen jeweils für verschiedene Datenstrukturen angepaßt werden (Überlagerung von Funktionen). c) zusammenhängende Routinen (Pakete): Module, wie z.B. "Suche" hängen meist von den ihnen vor gelagerten Modulen ab: - Erzeugen der Tabelle - eintragen - löschen von Elementen Wenn man also eine Tabelle durchsuchen will, muß man wissen, wie sie erzeugt wurde. Einzelne Routinen in Bibliotheken abzulegen, ist nicht besonders sinnvoll (kann man denn tatsächlich alles berücksichtigen?). Andererseits sagt eine Suchroutine beispielsweise noch nichts über Tabellenerzeugung, Eintragen und Löschen aus. Pakete sind eine Lösung für das geschilderte Problem. Pakete können zusammenhängende Routinen zusammen mit Typ-, Konstanten- und Variablendeklaration unter einem Dach vereinen (Kapselung). In dem Suchbeispiel besteht das Paket aus der gesamten Implementierung des Tabellenprogramms. Ein wichtiger Begriff ist in diesem Zusammenhang gefallen: - Kapselung (oder Schaffung abstrakter Datentypen)

35

d) Darstellungsunabhängigkeit: Eine modulare Struktur sollte es ermöglichen, Module zu benutzen, ohne etwas über ihre Implementierung zu wissen. Darstellungsunabhängigkeit hängt mit - Typ-Variation und - Datenstruktur- und Algorithmen-Variation zusammen. Wenn z.B. verschiedene Suchalgorithmen vorhanden sind, sollte das Programm in der Lage sein, selbständig (zur Laufzeit) den geeigneten zu finden und auszuführen. Um diese höchste Form der Darstellungsunabhängigkeit zu erreichen, werden dezentrale Modularchitekturen aufgebaut. (Beispiel später). Beispiel:

Darstellungsunabhängigkeit welcher Suchalgorithmus welche Daten Datenstruktur- und Algorithmen-Variation Typ-Variation Festlegung erst zur Laufzeit Wie kann man dies erreichen? Eine Lösung dieses Problems ist die Überlagerung von Funktionen. Überlagerung löst vorwiegend das Problem der Datenstruktur- und Algorithmen-Variation. zweiter wichtiger Begriff: Überlagerung

Überlagerung Überlagerung von Funktions- Operartor- Variablennamen überlagerung überlagerung

36

Die Auswahl erfolgt durch das so genannte dynamische Binden (Binden zur Laufzeit). Eine zweite Lösung zur Realisierung von Darstellungsunabhängigkeit ist Generizität. Generizität: - Schaffung von parametrierten (generischen) Modulen (Schablonen) Diese Module sind selbst nicht ausführbar, sondern stellen nur ein Modulmuster dar. Beispiel: suche (Element): generisches Modul suche (Integer): 1. Exemplar suche (Real) : 2. Exemplar ... In C++ werden hierzu : - Funktionsschablonen - Klassenschablonen und - Containerklassen eingeführt. d) Gemeinsamkeit zwischen Teilbaugruppen: Dieses Problem ist von zentraler Bedeutung im Hinblick der Wiederverwendbarkeit von Modulen. Es geht darum, wohl strukturierte Modulsammlungen ohne unnötige Wiederholungen zu schreiben. Beispiel: Ein Suchalgorithmus ist für viele Datenstrukturen der gleiche, nur die Operationen unterscheiden sich. !(Ende der Datenstruktur) && n z.B. Ende des Moduls !(Element gefunden?)

37

Array verkettete Liste (einfach)

Datei

Beginn counter = 0; list = start.element; Öffnen der Datei

nächstes Element counter++; list = list.next; Fseek

Ende der Daten counter > anzahl? list == NULL? EOF

4.3.4 vorläufige Kurzzusammenfassung

- Modularität ist oberstes Ziel, um Software stabil zu machen - entscheidende Kriterien: - Wiederverwendbarkeit - Datensicherheit - Erweiterbarkeit - Wartbarkeit - wichtige Begriffe bis hier: - Kapselung - Überlagerung

4.4 funktionaler oder datenorientierter Entwurf

Am Anfang des Entwurfes muß jeder Entwickler für sich die Frage beantworten: Soll ich um Funktionen oder um Daten herum strukturieren? Wir haben also die wahl zwischen - funktionsorientierten oder - datenorientierten Entwurf. Folgende Frage ist dabei zu beantworten? WAS STEHT IM VORDERGRUND, WAS IST WEITGEHEND KONSTANT? Entweder die Aktion oder die Daten.

38

Beispiele:

Lfd. Nr. Beschreibung der Aufgabe

Was steht im Vordergrund

Wie entwerfen ?

1 Realisierung einer seriellen Datenübertragung (z.B. Kopplung zweier Rechner)

Aktion (z.B. Handshak), diese aber konstant was übertragen wird, ist egal (Festplatte, oder Disketten...)

funktions- orientiert daten- orientiert

2 Realisierung eines Lohnabrechnungs- programms für ein Unternehmen

Eingangsdaten bleiben weitgehend konstant (Bruttolohn, Arbeits- zeit) Funktionen können sich ändern (z.B. Soli- zuschläge, Anfertigung versch. Statistiken) oder kommen hinzu

daten- orientiert

3 Simulation von technischen Prozessen (typisch für OOP)

Eingangsdaten (z.B. Naturgesetze) bleiben konstant. Funktionen ändern sich ständig

daten- orientiert

4 BACKUP-RESTORE für SCSI-Streamer

1. Blickwinkel: wie serielle Daten- übertragung. Aktionen bleiben konstant. 2. Blickwinkel: Realisierung einer ganz allgemeinen SCSI-Schnittstelle (Daten bleiben konstant, Geräte ändern sich)

funktions- oder daten- orientiert daten- orientiert

39

Fazit: Manchmal wissen wir nicht, wie wir entwerfen sollen. Es scheint aber so, als ob (fast) jeder funktionsorientierte Entwurf auch datenorientiert erfolgen kann. Der umgekehrte Weg scheint dagegen mit großen Problemen behaftet zu sein. Es sollte immer ein Schlüsselelement als Entscheidungsgrundlage herangezogen werden. Das wichtigste Schlüsselelement ist hierbei das - Stetigkeitsprinzip. Funktionen können sich im Laufe der Zeit radikal ändern. Sehr viel mehr Beständigkeit findet man in den Datenstrukturen, auf denen gearbeitet wird (hohes Abstraktionsniveau bei Daten vorausgesetzt). Deshalb ist es sehr oft von Vorteil, Daten zur Systemstrukturierung zu verwenden. Eine erste Definition für objektorientierten (datenorientierten) Entwurf kann lauten: Objektorientierter Entwurf ist diejenige Methode, die zu Softwarearchi- tekturen führt, die auf dem von jedem System oder Teilsystem bearbeiteten Objekten beruhen (und nicht auf "der" Funktion, die das System realisiert). Also! Frag nicht erst, WIE das System etwas tut: Frag, WORAN es etwas tut! Bevor nun auf das objektorientierte Design eingegangen wird, sind an dieser Stelle einige Grundbegriffe zu klären.

5 Grundbegriffe der OOP (Zusatzliteratur)

a) Objektklassifizierung: Ein Objekt ist in der OOP schlicht und einfach ein Element. Es kapselt sowohl Funktionen als auch Daten, d.h. ein Objekt enthält Informationen und "weiß", wie es mit ihnen umgehen muß. Kapselung: "Als Bestandteil des Softwaredesigns liegt die Funktion der Kapselung in der Unterstützung der Abstraktion"/1/. Es wird quasi ein Kreis um miteinander verwandte Ideen gezogen. Kapselung transferiert viele Elemente in ein Element, wodurch komplexe Vorgänge vereinfacht, abstrahiert werden.

40

Verbergen von Informationen (Information hidding): Aussage, inwieweit gekapselte Elemente für andere Objekte sichtbar sind.Ein Objekt verfügt über eine öffentliche Schnittstelle und eine private Repräsentation. öffentliche Schnittstelle: Objekt zeigt an, daß es bestimmte Operationen ausführen und Informationen liefern kann. Öffentliche Repräsentation: Das Objekt zeigt aber nicht, wie es dies tut. Der große Vorteil liegt darin, daß bei Veränderung der privaten Repräsentation andere Objekte nicht in Mitleidenschaft gezogen werden. b) Klassen: Eine Klasse ist eine gekapselte Datenstruktur, die sowohl Daten als auch die Funktionen zur Verarbeitung dieser Daten enthält. Sie beschreibt die Eigenschaften gleicher Objekte. Ein Objekt dagegen ist eine Instanz einer Klasse (ähnlich des Datentypes „struct“). In der objektorientierten Sprachsyntax spricht man bei den Funktionen zur Verarbeitung von Daten nicht von Funktionen oder Prozeduren, sondern von Methoden. Damit wird eigentlich schon Sinn und Zweck der Kapselung ausgedrückt. In einer Klasse befinden sich Daten und die verschiedenen Methoden ihrer Handhabung. Methoden gehören zur privaten Repräsentation einer Klasse. Damit wird aber auch schon eines deutlich: Es scheint so zu sein, daß man auf die Daten einer Klasse nur über die dort implementierten Methoden Zugriff hat. Dies würde einen großen Fortschritt im Hinblick oben zitierter Datensicherheit bedeuten. Zum Abschluß faßt eine graphische Übersicht das Prinzip der Kapselung zusammen.

Klassen sind der eigentliche Unterschied zur herkömmlichen Programmierstrategie. Die Verarbeitung von Daten geschieht im Objekt und verteilt sich nicht mehr über das gesamte Programm.

41

Zur Veranschaulichung des Begriffes "Klasse"

Klasse

Daten der Klasse

Methode 1 Methode 2

Schnittstelle zur Außenwelt Botschaft Botschaft

42

c) Vererbung: Eine Klasse kann als Einschränkung oder Erweiterung einer anderen Klasse definiert werden. Man Klassen entwerfen, die Erben von mehr als einer Klasse sind und mehr als einmal von einer Klasse erben. Wenn eine Klasse B von einer Klasse A erbr, dann sind automatisch alle Dienste von A in B verfügbar, ohne daß sie noch einmal definiert werden müssen. Die Klasse B darf aber neue Merkmale hinzufügen. Gleichfalls darf die Klasse B einige Dienste von A verändern. Dies geschieht durch Überlagerung oder Redefinition von Diensten (Methoden). Diese Technik unterstützt das Offen - Geschlossen -Prinzip Offen: Es gibt keine Gewähr, daß von Anfang an alle Dienste vorgesehen wurden (oder an sie gedacht worden ist) Geschlossen: Erben nützen aber fertige Dienste, ohne das Fahrrad neu zu erfinden. Beispiel: Fenstertechnik Zu Anfang war nur das Fenster mit seinen Koordinaten als Parameter. Dann kommt die Farbe hinzu, dann ein Fenstertext und dann, ... . Herkömmlich müßten die jeweiligen Prozeduren neu geschrieben werden, bereits lauffähige Module werden wieder verändert. Jetzt ist dies nicht mehr erforderlich. Vererbung ist also ein weiteres entscheidendes Kriterium für Erweiterbarkeit und Wiederverwendbarkeit.

43

Zur Veranschaulichung des Prinzipes der

Vererbung

Basisklasse

(Fenster: Lage und Größe)

abgeleitete Klasse

(Fensterfarbe)

abgeleitete Klasse

(Fenstertext)

abgeleitete Klasse erbt

alles von der (Basisklasse)

abgeleitete Klasse erbt

alles von der (Basisklasse)

abgeleitete Klasse erbt

alles von der (Basisklasse)

44

Vererbung (abgeleitete Klassen werden auch wieder zu

Basisklassen)

Basisklasse

(Fahrzeuge)

abgeleitete Klasse

(LKW)

abgeleitete Klasse

(PKW)

abgeleitete Klasse erbt

alles von der (Basisklasse)

abgeleitete Klasse: BMW (keine neue Basisklasse)

abgeleitete Klasse:OPEL (auch neue

Basisklasse)

OMEGA ASTRA VECTRA

45

Man kann Klassen deklarieren, die mehr als von einer Klasse erben. Dies nennt man Mehrfachvererbung. Klasse 1 Klasse 2 Mehrfachvererbung Zahlreiche Softwaretechnologen lehnen dieses Mehrfachvererbungskonzept ab, weil sie behaupten, das dabei stets Redundanz auftritt. So wird dieses Konzept in JAVA nicht unterstützt. d) abstrakte Klassen und Vererbung: Nicht jede Klasse erzeugt eine Instanz (also ein Objekt) von sich selbst. Sie existieren lediglich, damit ihr Verhalten von anderen übernommen werden kann.

abstrakte Klasse vollständig funktional implementiert Schablone (allgemeine Meldungen werden bereitgestellt. Die Unterklassen müssen spezielle Implementa- tionen dieser Meldungen bereitstellen.

46

Beispiel:

STREAMER (abstrakt) - online - lesen - schreiben - warten - offline - kein Tape paralleler Streamer SCSI-Streamer (spezielle Implementierung) (spezielle Implementierung) e) Polymorphismus: Polymorphismus in Verbindung mit der OOP hat mehrfache Bedeutung. 1. Überlagerung von Funktionen (gleicher Name, aber unterschiedliche Wirkung) 2. Überlagerung von Operatoren (Anwendung von Operatoren auf Klassen und deren Methoden) 3. Überlagerung einer Botschaft (Die Zuordnung einer Botschaft zu einem Objekt erfolgt erst beim Programm- durchlauf. Dieses Verfahren nennt man „späte Bindung“ und ermöglicht die Erstellung sehr effizienter Programmstrukturen)

47

6 Der objektorientierte Entwurfsprozeß (Zusatzliteratur)

Der objektorientierte Entwurfsprozeß besteht im Wesentlichen aus drei Phasen: - Bestimmung von Klassen - Bestimmung von Operationen und Verantwortlichkeiten - Bestimmung von Wechselbeziehungen (Schnittstellenbeschreibung)

6.1 Die Bestimmung von Klassen

Der Prozeß des objektorientierten Designs beginnt mit dem Studium der Aufgabenspezifikation (z.B.: Pflichtenheft), da dies meist die einzige Eingabe ist, über die der Programmierer verfügt. Falls es die Situation erlaubt (ähnliche Aufgabenstellung wurde schon einmal bearbeitet) können im Anschluß drei Schritte ausgeführt werden: a) erster Schritt (Kritik vorhandener Entwürfe): Bereits vorhandene Entwürfe sollten bewertet und analysiert werden. Wenn für ein Problem eine bestimmte Anzahl von Klassen vorgeschlagen wurde, dann können diese Klassen hinsichtlich ihrer Modularität untersucht werden. Kriterien sind: - sind es eigenständige, in sich geschlossene Module? - wie sind die Schnittstellen organisiert? - ...etc. b) zweiter Schritt (wiederverwendbare Klassen): - was ist bereits an Klassen vorhanden? - welche Klassen können verwendet werden bzw. müssen "lediglich" angepaßt werden (also Vererbung und Polymorphismus) c) dritter Schritt (externe (physikalische) Objekte nutzen): Viele Klassen beschreiben das Verhalten externer Objekte aus der modellierten Wirklichkeit, z.B.: - Fahrzeug - Flugzeug - Fenster, Tastaturen, Mäuse, etc... Diese Beschreibung liefert oft schon die Schlüsselideen für ein System (wie gleich gezeigt wird).

48

Danach erfolgt die detaillierte Auseinandersetzung mit der Aufgabenstellung. d) vierter Schritt (detaillierte Betrachtung der Anforderungsspezifikation): 1. Suchen aller Hauptwörter 2. Umwandlung von Mehrzahl in Einzahl (bei Hauptwörtern). Erstellung einer ersten Liste möglicher Klassen 3. Liste aller Hauptwörter offensichtliche Klassen offensichtlicher Unsinn /1/ Wie kann man nun eine relevante Auswahl möglicher Klassen erhalten? 4. Eliminierung irrelevanter Aspekte 5. Streichung von Hauptwörtern, die nicht moduliert werden (z.B.: Der Benutzer muß ...) 6. Streichung von Hauptwörtern, die mit der Aufgabenstellung an sich nichts zu tun haben 7. Streichung von Hauptwörtern, die die Aufgabe selbst repräsentieren 8. falls es mehrer Hauptwörter gibt, die das gleiche widerspiegeln, wählen Sie das aussagekräftigste aus und streichen Sie alle anderen Hauptwörter 9. sehr genaue Analyse von Adjektiven Adjektiv irrelevant Hinweis auf andere Hinweis auf andere Objekte Benutzung des gleichen Objektes eliminieren separate Klasse keine neue Klasse

49

10. Analyse aktiver und passiver Sätze (eventuelle Umwandlung in aktive Sätze, um neue Hauptwörter zu finden) 11. Feststellung von Attributen oder Zusatzinformationen (liefert erste Informationen über Daten einer Klasse oder deren Schnittstelle) 12. Auswahl bedeutsamer Operationen und Zuordnung von Objekten 13. gesunder Menschenverstand (viel Erfahrung und Übung) Im Ergebnis dieser 13 Schritte entsteht eine vorläufige Liste von Klassen, wobei diese Schritte in der Regel mehrfach durchgeführt werden müssen. Diese allgemeinen Aussagen sollen nun anhand eines konkreten Beispiels veranschaulicht werden..

50

Ein Beispiel - Simulation der Vorgänge in einem Fernsehgerät

Nachdem zunächst die Schritte 1-3 abgearbeitet wurden, erfolgt nun die Analyse der Aufgabenstellung anhand des Pflichtenheftes: 1. , 2., 3. Suchen aller Hauptwörter, Umwandlung Mehrzahl-->Einzahl, Erstellung Klassenliste:

Substantive

Substantive

Satanlage Demodulation der DF

Kabelanschluß Rauschanteile

Bild- und Tonsignal NF-Signal

Erste Baugruppe Lautsprecher

Fernsehempfänger sehr komplexe Baugruppe

Kabeltuner Signale

Tuner Ansteuerung

Vorstufe Bildröhre

Mischstufe Bildinformation

Oszillator Steuerung

Aufgabe Elektronenstrahl

Bild- und Tonsignal Teilbaugruppe

Bildzwischenfrequenz Videoverstärker

Tonzwischenfrequenz Farbdecoder

Frequenz Synchronisations- stufe

Fernsehgerät Zeilenendstufe

ZF-Verstärker Vertikalablenkung

Zwischenfrequenz Fernsehnorm

Ausgang Farbnorm

Verstärker Bildröhrentyp

Demodulation Stromversorgungsteil

FBAS-Signal Gleichspannung

Differenzfrequenz Schwankung

Ton Fernsehgerät

Tonsignal Überlast

spezielle Baugruppe Kurzschluß

Beseitigung

Bildträger

Nicht alle Hauptwörter können und müssen Klassen werden.

51

Simulation der Vorgänge in einem Fernsehgerät (Auffinden von Klassen und Methoden)

Pflichtenheft (Auszug): Von der Satanlage bzw. dem Kabelanschluß gelangt das modulierte Bild- und Tonsignal zur ersten Baugruppe eines Fernsehempfängers, dem Kabeltuner. Der Tuner besteht ganz allgemein aus der Vorstufe, einer Mischstufe und einem Oszillator. Der Tuner hat prinzipiell die Aufgabe, das Bild -und Tonsignal in eine Bildzwischenfrequenz (BZF) und eine Tonzwischenfrequenz (TZF) zu wandeln. Diese Frequenzen sind bei allen Fernsehgeräten gleich. Der sich anschließende ZF-Verstärker verstärkt zunächst beide Zwischenfrequenzen. Am Ausgang dieses Verstärkers erfolgt eine Demodulation. Einerseits bildet sich hierbei das sogenannte FBAS-Signal (Farb-Bild-Austast-Synchron) und eine Differenzfrequenz (DF), die den modulierten Ton enthält. Das Tonsignal wird in einer speziellen Baugruppe decodiert (Beseitigung des Bildträgers, Demodulation der DF). Rauschanteile werden ebenfalls ausgefiltert. Anschließend wird das hörbare NF-Signal verstärkt und dem Lautsprecher zugeführt. In einer weiteren, sehr komplexen, Baugruppe werden letztendlich die Signale zur Ansteuerung der Bildröhre (Bildinformation, Steuerung des Elektronenstrahls) bereitgestellt. Wichtige Teilbaugruppen sind dabei der Videoverstärker, der Farbdecoder, die Synchronisationsstufe, die Zeilenendstufe und die Vertikalablenkung. Je nach Fernsehnorm, Farbnorm und Bildröhrentyp unterscheiden sich diese Teilbaugruppen voneinander. Letztendlich besteht jedes Fernsehgerät aus einem komplexen Stromversorgungsteil, der u.a. alle Gleichspannungen bereitstellt. Gleichzeitig müssen hier Netzspannungsschwankungen ausgeglichen werden und das Gerät muß gegen Überlast und Kurzschluß gesichert sein.

52

4. Eliminierung irrelevanter Aspekte Satanlage Kabelanschluß Signale Ansteuerung Steuerung Schwankung 5. Streichung von Hauptwörtern, die nicht moduliert werden Aufgabe Ausgang Beseitigung 6. Streichung von Hauptwörtern, die mit der Aufgabenstellung an sich nichts zu tun haben ?? in diesem Beispiel nicht deutlich erkennbar 7. Streichung von Hauptwörtern, die die Aufgabe selbst repräsentieren Fernsehempfänger Fernsehgerät (doppelt) 8. falls es mehrer Hauptwörter gibt, die das gleiche widerspiegeln, wählen Sie das aussagekräftigste aus und streichen Sie alle anderen Hauptwörter a) Kabeltuner --> Kabeltuner Tuner b) Bild- und Tonsignal (doppelt) c) Bild - ZF --> Bild - ZF Ton - ZF --> Ton - ZF Frequenz Zwischenfrequenz d) ZF - Verstärker --> ZF - Verstärker Verstärker e) Ton --> Ton Tonsignal f) Bildröhre Bildröhre Bildröhrentyp

53

9. sehr genaue Analyse von Adjektiven erste Baugruppe spezielle Baugruppe sehr komplexe Baugruppe Teil- Baugruppe a) erste --> Hinweise auf jeweils andere spezielle Objekte, also jeweils sehr komplexe separate Klassen b) sehr komplexe --> Hinweis auf andere Benutzung Teil des gleichen Objektes (deutet auf abstrakte oder Basis- klasse hin, z.B.: unterschiedliche Farbnorm) c) erste bezieht sich auf Kabeltuner, also Streichung "erste Baugruppe" 10. Analyse aktiver und passiver Sätze (eventuelle Umwandlung in aktive Sätze, um neue Hauptwörter zu finden) sind so offensichtlich nicht vorhanden 11. Feststellung von Attributen oder Zusatzinformationen (liefert erste Informationen über Daten einer Klasse oder deren Schnittstelle) a) Bild- und Tonsignal Eingangssignal des Kabeltuners (Streichung) Bild - ZF, Ton - ZF Ausgangsgrößen des Tuners (Streichung) b) FBAS-Signal Ausgangssignale des ZF-Verst. Differenzfrequenz (Streichung der 3 Größen) (modulierter Ton ist redundant hierzu) c) HAUSAUFGABE, was "spezielle Baugruppe" betrifft d) sehr komplexe Baugruppe

54

e) Gleichspannung Ausgangssignal Stromversorgung (Streichung) Überlast, Kurzschluß eindeutig Attribute (Daten einer Klasse --> Streichung) 12. Auswahl bedeutsamer Operationen und daraus resultierende Zuordnung von Objekten Liste potentieller Klassen Kabeltunerklasse ZF-Verstärkerklasse spezielle Baugruppenklasse (Tonerzeugung) sehr komplexe Baugruppenklasse, die Teilbaugruppenklassen enthält Stromversorgungsklasse Liste potentieller Unterklassen (oder eben doch eigenständiger Klassen) Bildröhre Videoverstärker Farbdecoder Synchronisationsstufe Zeilenendstufe Vertikalablenkung Liste restlicher Hauptwörter, die momentan noch nicht gedeutet werden können Vorstufe Mischstufe Oszillator Demodulation Bildträger Rauschanteile NF-Signal Lautsprecher Bildinformation Elektronenstrahl Fernsehnorm Farbnorm 13. gesunder Menschenverstand (viel Erfahrung und Übung) Bildträger Bildinformation Elektronenstrahl Rauschanteile NF-Signal Fernsehnorm Farbnorm alle aufgeführten Worte (Klassen) streichen

55

Was ist übrig geblieben?

Hauptwörter

Hauptwörter

Satanlage Demodulation der DF Kabelanschluß Rauschanteile

Bild- und Tonsignal NF-Signal

erste Baugruppe Lautsprecher Fernsehempfänger sehr komplexe

Baugruppe

Kabeltuner Signale

Tuner Ansteuerung

Vorstufe Bildröhre

Mischstufe Bildinformation

Oszillator Steuerung

Aufgabe Elektronenstrahl

Bild- und Tonsignal Teilbaugruppe Bildzwischenfrequenz Videoverstärker Tonzwischenfrequenz Farbdecoder Frequenzen Synchronisations-

stufe Fernsehgerät Zeilenendstufe

ZF-Verstärker Vertikalablenkung

Zwischenfrequenz Fernsehnorm

Ausgang Farbnorm

Verstärker Bildröhrentyp

Demodulation Stromver- sorgungsteil

FBAS-Signal Gleichspannung

Differenzfrequenz Schwankungen

Ton Fernsehgerät

Tonsignal Überlast

spezielle Baugruppe

Kurzschluß

Beseitigung Bildträger

56

6.2 Die Bestimmung von Operationen / Verantwortlichkeiten

Verantwortlichkeiten setzen sich aus zwei Aspekten zusammen: - Wissen (Eigenschaften), das eine Klasse (Objekt) besitzt - Operationen Es erfolgt eine erneute detaillierte Auseinandersetzung mit der Anforderungsspezifikation. Welche Schritte sind diesmal erforderlich? 1. Suchen aller Verben 2. Umwandlung in die Gegenwartsform Erstellung einer ersten Liste möglicher Klassen 3. Liste aller Verben offensichtliche Methoden offensichtlicher Unsinn /1/ Wie kann man nun wiederum eine relevante Auswahl treffen? 4. Streichung von Verben, die keine Information zum System bzw. zu Operationen liefern 5. Analyse des Namens bereits gefundener Klassen, Streichung zugehöriger Verben und entsprechende Klassenzuweisung (möglicher Hinweis auf Operationen / Verantwortlichkeiten) 6. Zuweisung von Verben zu bereits gefundenen Klassen, Streichung dieser Verben 7. Analyse der Verben, die bisher keine Verwendung fanden 8. Umwandlung von Hauptwörtern in Verben, wenn diese Verhalten bereits gefundener Klassen widerspiegeln (Streichung aus der Liste von möglichen Klassen) Nun wieder zum Beispiel " Simulation der Vorgänge in einem Fernsehgerät" .

57

Simulation der Vorgänge in einem Fernsehgerät (Auffinden von Klassen und Methoden)

Pflichtenheft (Auszug): Von der Satanlage bzw. dem Kabelanschluß gelangt das modulierte Bild- und Tonsignal zur ersten Baugruppe eines Fernsehempfängers, dem Kabeltuner. Der Tuner besteht ganz allgemein aus der Vorstufe, einer Mischstufe und einem Oszillator. Der Tuner hat prinzipiell die Aufgabe, das Bild -und Tonsignal in eine Bildzwischenfrequenz (BZF) und eine Tonzwischenfrequenz (TZF) zu wandeln. Diese Frequenzen sind bei allen Fernsehgeräten gleich. Der sich anschließende ZF-Verstärker verstärkt zunächst beide Zwischenfrequenzen. Am Ausgang dieses Verstärkers erfolgt eine Demodulation. Einerseits bildet sich hierbei das sogenannte FBAS-Signal (Farb-Bild-Austast-Synchron) und eine Differenzfrequenz (DF), die den modulierten Ton enthält. Das Tonsignal wird in einer speziellen Baugruppe decodiert (Beseitigung des Bildträgers, Demodulation der DF). Rauschanteile werden ebenfalls ausgefiltert. Anschließend wird das hörbare NF-Signal verstärkt und dem Lautsprecher zugeführt. In einer weiteren, sehr komplexen, Baugruppe werden letztendlich die Signale zur Ansteuerung der Bildröhre (Bildinformation, Steuerung des Elektronenstrahls) bereitgestellt. Wichtige Teilbaugruppen sind dabei der Videoverstärker, der Farbdecoder, die Synchronisationsstufe, die Zeilenendstufe und die Vertikalablenkung. Je nach Fernsehnorm, Farbnorm und Bildröhrentyp unterscheiden sich diese Teilbaugruppen voneinander. Letztendlich besteht jedes Fernsehgerät aus einem komplexen Stromversorgungsteil, der u.a. alle Gleichspannungen bereitstellt. Gleichzeitig müssen hier Netzspannungsschwankungen ausgeglichen werden und das Gerät muß gegen Überlast und Kurzschluß gesichert sein.

58

1. , 2., 3. Suchen aller Verben, Umwandlung in Gegenwartsform, Liste aller Verben:

Verben

Verben

gelangen verstärken

bestehen zuführen

haben bereitstellen

wandeln sind

sind unterscheiden

verstärken bestehen

bilden bereitstellen

enthalten ausgleichen

decodieren sichern

ausfiltern

4. Streichung von Verben, die keine Information zum System bzw. zu Operationen liefern gelangen haben sind bilden enthalten sind bestehen 5. Analyse des Namens bereits gefundener Klassen, Streichung zugehöriger Verben und entsprechende Klassenzuweisung (möglicher Hinweis auf Operationen / Verantwortlichkeiten) Kabeltuner keine Information ZF-Verstärker verstärken (Streichung aus Liste) spezielle Baugruppe Hausaufgabe (Tonerzeugung) (keine direkte Information aus Namen) sehr komplexe Baugruppe keine direkte Information Stromversorgungsteil keine direkte Information 6. Zuweisung von Verben zu bereits gefundenen Klassen, Streichung dieser Verben Kabeltuner wandeln ZF-Verstärker verstärken (aus Punkt 5) (Signale) bilden --> ("bilden" also voreilig gestrichen)

59

spezielle Baugruppe Hausaufgabe (Tonerzeugung) (decodieren, ausfiltern, verstärken zuführen) sehr komplexe Baugruppe (Signale) bereitstellen (Bilderzeugung) (erstmal hier nicht mehr) Stromversorgungsteil (Gleichspannung) bereitstellen ausgleichen sichern 7. Analyse der Verben, die bisher keine Verwendung fanden bestehen (Kabeltunerklasse) unterscheiden (sehr komplexe Baugruppe) bestehen: typisches Wort für eine " is-part-of" - Beziehung, d.h. alles was sich auf "bestehen" bezieht, ist Bestandteil der Klasse (entweder Daten oder Methoden) endgültige Streichung von

Vorstufe Mischstufe Oszillator

als mögliche Klassen unterscheiden: typisches Wort für eine " is-kind-of" - Beziehung, d.h. alles deutet auf eine Basisklassen / abgeleitete Klassen - Beziehung hin. (Bsp.: Farbdecoder (Basisklasse) PAL, SECAM, NTSC (Unterklassen) Da darüber hinaus bekannt ist, daß z.B. Videoverstärker, etc. ... recht komplex ist, stellen auch diese verbleibenden Hauptwörter Klassen dar, wobei sinnvoller Weise eine Oberklasse "sehr-komplexe Baugruppe" als Schablone dient. Dies erfüllt das Modulkriterium Generizität als Voraussetzung für Darstellungsunabhängigkeit und Wiederverwendbarkeit.

60

8. Umwandlung von Hauptwörtern in Verben, wenn diese Verhalten bereits gefundener Klassen widerspiegeln (Streichung aus der Liste von möglichen Klassen) Kabeltuner Vorstufe --> Signal vorverarbeiten und vorverstärken Mischstufe --> Signal mischen Oszillator --> oszillieren wandeln (aus Punkt 6) STREICHUNG aus Klassenliste ZF - Verstärker verstärken (aus Punkt 5) Demodulation --> demodulieren STREICHUNG aus Klassenliste spezielle Baugruppe Hausaufgabe (Tonerzeugung) (Demodulation der DF) --> demodulieren STREICHUNG aus Klassenliste sehr komplexe die Operationen der gefundenen Klassen Baugruppe "Videoverstärker, etc..." wurden (aus Zeitgründen) nicht näher spezifiziert. Gleiche Vorgehensweise wie bisher Stromversorgungsteil keine Umwandlung mehr möglich

6.3 Bestimmung von Wechselbeziehungen (Schnittstellenbeschreibung)

1. Suchen aller Eingangsparameter und Rückgabeinformationen einer

gefundenen Klasse. Diese sind entweder als öffentlich zu deklarieren oder durch eine öffentliche Methode zu verwalten.

2. Festlegung der Daten einer Klasse, die aus der Anforderungsspezifikation

unmittelbar hervorgehen..

61

62

Simulation der Vorgänge in einem Fernsehgerät (Auffinden von Klassen und Methoden)

Pflichtenheft (Auszug): Von der Satanlage bzw. dem Kabelanschluß gelangt das modulierte Bild- und Tonsignal zur ersten Baugruppe eines Fernsehempfängers, dem Kabeltuner. Der Tuner besteht ganz allgemein aus der Vorstufe, einer Mischstufe und einem Oszillator. Der Tuner hat prinzipiell die Aufgabe, das Bild -und Tonsignal in eine Bildzwischenfrequenz (BZF) und eine Tonzwischenfrequenz (TZF) zu wandeln. Diese Frequenzen sind bei allen Fernsehgeräten gleich. Der sich anschließende ZF-Verstärker verstärkt zunächst beide Zwischenfrequenzen. Am Ausgang dieses Verstärkers erfolgt eine Demodulation. Einerseits bildet sich hierbei das sogenannte FBAS-Signal (Farb-Bild-Austast-Synchron) und eine Differenzfrequenz (DF), die den modulierten Ton enthält. Das Tonsignal wird in einer speziellen Baugruppe decodiert (Beseitigung des Bildträgers, Demodulation der DF). Rauschanteile werden ebenfalls ausgefiltert. Anschließend wird das hörbare NF-Signal verstärkt und dem Lautsprecher zugeführt. In einer weiteren, sehr komplexen, Baugruppe werden letztendlich die Signale zur Ansteuerung der Bildröhre (Bildinformation, Steuerung des Elektronenstrahls) bereitgestellt. Wichtige Teilbaugruppen sind dabei der Videoverstärker, der Farbdecoder, die Synchronisationsstufe, die Zeilenendstufe und die Vertikalablenkung. Je nach Fernsehnorm, Farbnorm und Bildröhrentyp unterscheiden sich diese Teilbaugruppen voneinander. Letztendlich besteht jedes Fernsehgerät aus einem komplexen Stromversorgungsteil, der u.a. alle Gleichspannungen bereitstellt. Gleichzeitig müssen hier Netzspannungsschwankungen ausgeglichen werden und das Gerät muß gegen Überlast und Kurzschluß gesichert sein. Bitte vervollständigen Sie 2 und 3) selbständig. Es dient Ihrer Übung und Sicherheit bei zukünftigen Entwürfen.

63

6.4 Vorläufiges Design für die Fernsehgerätesimmulation

Klasse1: Kabeltuner-Klasse (wichtige Operationen --> Bereitstellung wichtiger Signale) Sinnvoll kann die Umwandlung von Substantiven zu Verben sein. Einerseits verringert sich die Anzahl der Klassen, andererseits werden wesentliche Operationen herausgestellt. Methoden: Vorstufe (Signal vorverarbeiten) Mischstufe (Signale mischen) Oszillator wandeln (Erzeugung von Bild - ZF, Ton-ZF) Schnittstelle: Eingang (SAT) Bild-ZF Kabeltuner Ton-ZF Klasse2: ZF-Verstärker-Klasse (wichtige Operationen --> Verstärkung, Erzeugung von Bild und kod. Ton) Methoden: verstärken demodulieren (erneut Umwandlung eines Substantivs) Schnittstelle: Bild - ZF kodierter Ton (DF) ZF - Verstärker Ton - ZF FBAS - Signal

64

Klasse3: Stromversorgungseinheit - Klasse Methoden: erzeugen (der benötigten Gleich und Wechsel- spannungen) ausgleichen (von Schwankungen) sichern (gegen Kurzschluß und Überlast) Schnittstelle: Netz 220V alle Spannungen Stromversorgungseinheit usw. Eine Klasse stellt eine Besonderheit dar, die der "sehr komplexen Baugruppe". Die einzelnen Teilbaugruppen sind derart komplex, daß es hier sinnvoll erscheint, eine weitere Klassenunterteilung zu wählen. Beispiel: Klasse 5: Bilderzeugungsklasse Bilderzeugungsklasse (z.B. Schablone) z.B. Farbdecoder ... andere Klassen PAL SECAM NTSC Vererbung Fazit aber trotz allem: Erfahrung, Erfahrung, Erfahrung...

65

7 Entwicklung und Einordnung von C++ (Zusatzliteratur)

C++ ist im Gegensatz zu z.B. Smalltalk keine rein objektorientierte Sprache. C++ beinhaltet nach wie vor viele Teile eines prozeduralen Sprachkonzeptes (Datentypen). Es enthält aber die wesentlichsten Bestandteile einer objektorientierten Sprache. C++ existiert seit 1983 und wurde von Bjarne Stroustrup in den AT&T-Bell Laboratorien entwickelt. In der ersten kommerziellen Version ist C++ seit 1989 erhältlich. C++ ist eine Erweiterung der Sprache C zu einer modernen objektorientierten Sprache. Das „ ++ “ ergibt sich aus dem Inkrementoperator der Sprache C, wodurch ebenfalls die Erweiterung zum Ausdruck gebracht werden soll. Heute existierende Compiler sind auf allen Betriebssystemen vorhanden (Borland, Zortech, GNU C++, etc). C++ ist abwärts kompatibel zu C. Dies scheint wohl der Hauptgrund für die Akzeptanz von C++ zu sein.

C++ hat zwei wesentliche Nachteile, die sehr oft zu schwerwiegenden und kaum nachvollziehbaren Fehlern führt. - Zeiger (Verletzung des Geheimnisprinzips) (Klassenstrukturen und ihre Autonomie können zerstört werden) - keine automatische Speicherverwaltung (Nachteile bereits erläutert)

66

Einordnung objektorientierter Sprachen

objektbasierte Sprachen

objektorientierte Sprachen

Kap1,5,Wenzel

z.B. MODULA 2 (1978)

z.B. Simula(1963) & Smalltalk(1982)

z.B. ADA

(1980)

z.B. TURBO-

PASCAL(1985) & C++ (1983)

67

7.1 Ein -/ Ausgabe in C++

Dieser Abschnitt dient der unmittelbaren Vorbereitung zur Arbeit mit C++. Es muß an dieser Stelle auf wesentliche Implementierungen verzichtet werden, da des sich bei den Standardfunktionen zur Ein - und Ausgabe bereits um Klassen handelt. Gleichfalls ist es hier noch nicht möglich, alle Möglichkeiten der Anwendung dieser Standardfunktionen aufzuzeigen. Ein Benutzung von „printf“ und „scanf“ ist zwar weiterhin möglich, sollte aber im Hinblick der Nutzung von objektorientierten Methoden vermieden werden.

Standardklassen zur E/A - Arbeit unter C++

Klassentyp Anwendung Wo definiert?

istream ostream iostream

Eingabe (Standard) Ausgabe Ein- und Ausgabe

<iostream> // oder .h

ifstream ofstream fstream

Eingabe (Datei) Ausgabe Datei- E-/A

<fstream> // oder .h

7.1.1 Die Standardausgabe unter C++

Analog des Konzeptes von C werden wiederum alle Funktionen in Header untergebracht. Diesmal handelt es sich jedoch um Klassen, die ein erheblich größeren Umfang hinsichtlich der Anwendungsfälle haben als es in C der Fall war. Zur Ausgabe eines Datenstromes auf dem Monitor existiert das Standardobjekt: cout ( ostream, <iostream> ) // oder iostram.h Die Wirkung dieses Objektes ist mit „printf“ zu vergleichen. Syntax: cout << ausgabewert << ausgabewert << ...; cout << manipulator <<ausgabewert << ...; Der „<<-Operator“ ist dabei überlagert (Polymorphismus) und ist für jeden Wert neu zu schreiben. Manipulatoren dienen zur Typumwandlung eines Wertes (hex,oct,...) und erfüllen weitere Aufgaben. Die %-Schreibweise (printf) entfällt.

68

7.1.2 Die Standardeingabe unter C++

Zur Eingabe eines Datenstromes von der Tastatur existiert das Standardobjekt: cin ( istream, <iostream> ) // oder iostream.h Die Wirkung dieses Objektes ist mit „scanf“ zu vergleichen. Syntax: cin >> eingabewert >> eingabewert >> ...; cin >> manipulator >> eingabewert >> ...; Der „>>-Operator“ ist ebenfalls überlagert.

69

Beispiele zur einfachen Handhabung des Objektes COUT

// programm demonstriert die einfache anwendung des objektes // "cout" zur ausgabe auf dem monitor // // #include <iostream> // E/A-arbeit using namespace std; // Erläuterung: int zahl; char *_string="\nhallo\nwie geht es euch\n"; main() { // ausgabe eines wertes (ueber variable bzw. direktwert) // zu beachten ist, dass der '<<'-operator ueberlagert ist // und immer neu einzufuegen ist, sofern ein neuer // datenstrom ausgegeben werden soll (mehrere werte, // wechsel der datentypen,etc.) zahl=13; cout << zahl << 12; // erst die zahl, dann die 12

// kombinierte ausgabe von werten bzw. variablen und // strings cout << "der wert fuer zahl ist : " << zahl; // einbeziehung von steuerzeichen zur ausgabe, wie sie // bereits aus C bekannt sind (printf) cout <<"\nder wert fuer zahl ist : " << zahl <<"\n";

70

Beispiele zur einfachen Handhabung des Objektes COUT (Fortsetzung)

// formatierte ausgabe eines wertes in der gewuenschten // form im gegensatz zu C erfolgt die konvertierung hier nicht // ueber die %-darstellung sondern mittels spezieller // manipulationsfunktionen. die standardeinstellung ist // dezimal (dec).die jeweils letzte einstellung behaelt ihre // gueltigkeit cout <<"\nzahl als octale zahl " << oct << zahl; cout <<"\nzahl als hexadezimale zahl " << hex << zahl; cout <<"\nzahl als dezimale zahl " << dec << zahl; // aehnlich wie in PASCAL erfolgt in c++ eine automatische // typerkennung. es braucht also nicht der datentyp // angegeben zu werden, sofern typuebereinstimmung // vorliegt. // beispiel: ausgabe eines strings cout <<"\n" <<_string; }

71

Beispiele zur einfachen Handhabung des Objektes CIN

// programm demonstriert die einfache anwendung des objektes // "cin" zur eingabe von der tastatur // #include <iostream.h> // klasse zur E/A-arbeit auf // standardgeraet // diesmal mit „.h“ #include <stdlib.h> // speicherreservierung string int zahl1,zahl2; char *_string; main() { // eingabe eines wertes von der tastatur // diesmal ist der >>-operator ueberlagert und zeigt an, wem // etwas zugewiesen wird. bei mehrfachen zuweisungen // muss auch hier jedesmal der operator angegeben werden cin >> zahl1; cout <<zahl1 <<'\n'; // eingabekontrolle // mehrfache eingabe in einer zeile heisst mehrfaches // schreiben des ueberlagerten operators cin >> zahl1 >> zahl2; cout <<zahl1 << zahl2;

72

Beispiele zur einfachen Handhabung des Objektes CIN (Fortsetzung)

// eingabe eines wertes in einem bestimmten format cin >> hex >> zahl1 >> oct >> zahl2; cout << zahl1 << zahl2; // ausgabe erfolgt aber dezimal // auch cin fuehrt eine automatische typanpassung durch // beispiel: eingabe eines strings // beachte!! bevor der string (vereinbart als zeiger ) // eingegeben werden kann, muss dem zeiger speicher // reserviert werden. dies kann ueber die funktion "calloc" // erfolgen. // aber!!! // malloc liefert einen typenlosen zeiger zurueck, der die // startadresse des speichers enthaelt. in c ist die zuweisung // eines typenlosen zeigers an einen anderen zeigertypen // gestattet. in c++ erfolgt, wie bereits erwaehnt, eine strenge // typkontrolle. aus diesem grund muss der cast-operator // anwendung finden (explizite typkonvertierung) _string=(char *)malloc(20*sizeof(char)); // speicherreservierung // und typkonvertierung cin >> _string; cout <<_string; }

73

8 Klassen in C++ (Zusatzliteratur, Zusatzinformation)

8.1 Grundbegriffe (Wiederholung)

Eine Klasse ist eine vom Programmierer definierte Datenstruktur und stellt eine Erweiterung der „struct“-Konstruktion aus C dar. Ein Objekt ist eine Instanz dieser Klasse. Klassen können demnach mehrere Objekte haben. Klassen enthalten sowohl Daten als auch Funktionen zum Umgang mit diesen Daten. Man spricht in diesem Zusammenhang von Kapselung. Die Funktionen einer Klasse werden Memberfunktionen oder Methoden genannt. Der Informationsaustausch in der OOP erfolgt über Botschaften. Sofern eine Botschaft an ein Objekt gesendet wird, wird eine entsprechende Memberfunktion des Objektes aktiviert. Der Rückgabewert dieser Funktion stellt die Quittung des Objektes dar. Im Zusammenhang mit der OOP stellt das „Information Hidding“ eine besondere Komponente im Hinblick der Datensicherheit dar. Im Gegensatz zum Datentyp „struct“ ist es bei Klassen möglich, die einzelnen Komponenten in private protected (wie private, aber in abgeleiteten Klassen sichtbar) und public aufzuteilen. Private Komponenten einer Klasse ( Daten und Methoden ) können nur von den Methoden dieser Klasse aus angesprochen werden. Sie sind also der Öffentlichkeit nicht zugänglich. Öffentliche Strukturen ( public ) sind allen zugänglich und unterliegen keinen Einschränkungen. Damit bilden diese Komponenten die Schnittstelle zur Außenwelt. Man spricht hierbei auch von einem so genannten „Methodenprotokoll“.

74

8.2 Syntax in C++

Syntax: class <bezeichner> { public: ... // öffentliche Komponenten ... // andere Kommentarsyntax // public sollte immer am Anfang einer Klasse // stehen private: ... // private Komponenten ... }; // oder hier schon Vereinbarung // von Objekten der Klasse Ein Klasse wird demnach mit dem Schlüsselwort „class“ eingeleitet. Es folgen die öffentlichen bzw. privaten Komponenten, wobei die Reihenfolge beliebig ist. Der syntaktische Aufbau einer Klasse ist also dem Datentyp „struct“ sehr ähnlich.

8.2.1 Daten in einer Klasse

Datenelemente werden entsprechend den Regeln der Sprache C vereinbart. In Klassen allerdings dürfen Datenelemente nicht initialisiert (Wertzuweisung) werden. Daraus folgt, daß Konstanten (const) nicht erlaubt sind. Alle Daten können von einem beliebigen Typ sein. Insbesondere sind auch Felder, Strukturen, Zeiger oder andere Klassen als Datentyp zulässig. Sofern die Daten als „privat“ deklariert sind, können nur die Methoden der Klasse auf diese zugreifen. Dies stellt eine erste Form des „Information hidding“ dar. Daten sollten in der Mehrzahl Daten als privat deklariert werden.

8.2.2 Methoden einer Klasse

Merkmale von Methoden in C++: - der Gültigkeitsbereich beschränkt sich nur auf die Klasse - Methoden können automatisch auf alle Daten der Klasse zugreifen ohne diese zusätzlich zu vereinbaren. - weitere Zugriffsmöglichkeiten ergeben sich bei der Behandlung von Vererbungsmechanismen

75

Beispiel: Demonstration zur Syntax von Klassen class test1 // klassendeklaration { public: void erg(void); // prototyp private: float re,im; // realteil und imaginärteil }; // noch keine objekte vereinbart Üblicherweise werden in den Klassen die Methoden nur als Prototypen angegeben, d.h. die Definition der Memberfunktionen (Anweisungsteil) geschieht in der Regel außerhalb des eigentlichen Klassenbereiches. Syntax:

Datentyp(derMethode) Klassenname::methodenname (parameterliste) { vereinbarungsteil; ... ... } Beispiel: Deklaration und Definition von Methoden einer Klasse class test1 { ... ... // wie eben }; void test1 :: erg(void) { cout << re << im; }

8.2.3 Objekte von Klassen

Objekte sind die Repräsentanten von Klassen (gleichzusetzen mit Instanzen bei Strukturen). Mittels der bekannten Operatoren kann dann auf die Elemente einer Klasse zugegriffen werden.

76

Sofern ein Objekt eine „normale“ Instanz einer Klasse ist, kommt der „Punkt“-Operator zur Anwendung, handelt es sich um eine Zeigervariable wird der „Pfeil“-Operator benutzt. Während man bei der Verwendung von Zeigern in C den entsprechenden Speicherplatz über malloc bzw. calloc reservieren mußte, werden in C++ die Operatoren new // Reservierung von Speicherplatz bzw. delete // Freigabe des Speicherplatzes verwendet. Syntax: <objektname> = new <klassenname>; delete <objektname>; Die Anwendung dieser beiden Operatoren ist notwendig, da diese eine korrekte Initialisierung der Objekte bei Verwendung von Konstruktoren gewährleisten. Konstruktoren und deren Bedeutung werden im nächsten Abschnitt behandelt. Zur Veranschaulichung der bisherigen Betrachtungen dient ein erstes vollständiges Beispiel (bsp_00.cpp):

8.3 Konstruktoren und Destruktoren

8.3.1 Konstruktoren

Ein Ziel in C++ besteht darin, Klassenobjekte wie Standarddatentypen einzusetzen. Bei der Verwendung von Klassenobjekten ist es bisher aber noch nicht möglich, eine Initialisierung der Daten wie beispielsweise bei Strukturen vorzunehmen. Beispiel: ... class test1 { private: float re,im; char *text; };

77

// bsp_00.cpp: erstes beispiel zum kapitel "klassen" // beispiel zeigt die deklaration einer klasse, den zugriff auf deren // elemente und verbotene anweisungen #include <iostream.h> // oder …<iostream> und dann // using namespace std; class test1 { public: void init(float rez,float imz); // prototyp init // init der variablen re und im void erg(); // prototyp erg private: float re,im; // realteil, imaginaerteil }; void test1::init(float rez,float imz) // definition der klassenmethode { re=rez; // zuweisung des realwertes im=imz; // zuweisung des imaginärwertes } void test1::erg(void) // definition der klassenmethode { cout << "\n\nder realteil betraegt: " << re; cout << "\nder imaginaerteil betraegt: " << im; } void main(void) { test1 k1, *k2; // definition von objekten k1.init(2,3); k1.erg(); // init, ausgabe der werte k2=new test1; // reservierung von speicher k2->init(4,5); k2->erg(); // zweites objekt (zeiger); delete k2; // freigabe des speichers // folgende anweisung wäre nicht möglich: // cout << k1.re; // re ist privat }

78

struct auchtest1 { float re,im; char *text1; }; ... auchtest1 a1 = { 1.0,2.0,“structur“}; // geht test1 k1 = { 1.0,2.0, „klasse“}; // geht nicht Die Objektdaten lassen sich deshalb nicht initialisieren, weil sie „private“ sind. Wären die Daten „public“ wäre dies möglich. Macht man nun die Daten einer Klasse ausschließlich „public“ entfällt einer der wichtigsten Ursachen der Einführung der OOP und im besonderen der Klassen --> das Verbergen von Daten. C++ kennt eine spezielle Memberfunktion, die spezielle Aufgaben bei der Erzeugung von Objekten übernimmt, sogenannte Konstruktoren. Diese Klassenmethode wird vom Compiler bei der Erzeugung eines Objektes in spezieller Form behandelt und automatisch aufgerufen. Der automatische Aufruf hat zur Folge, daß Konstruktoren für den Compiler besonders gekennzeichnet werden müssen. Eigenschaften von Konstruktoren 1. Eigenschaft: gleicher Name, wie der Name der Klasse Daraus ergibt sich nachstehende Syntax: Syntax: class <name> { public : ... <name>(<Parameterliste>); // konstruktor private: ... }; Beispiel: class test1 { public : test1 (float rez) {

re=rez; // definition in der klasse selbst

} private : float re,im; }; void main(void) { test1 k1(1.5); }

79

Konstruktoren

- automatischer Aufruf bei Objekterzeugung - gleicher Name wie die zugehörige Klasse - (fast) immer „public“ - expliziter und impliziter Aufruf möglich - Defaultwertfestlegung (nicht näher betrachtet) - kein Mehrfachaufruf für ein Objekt - Bedeutung von MALLOC und NEW - Standardkonstruktor - Überladung - Objekte in Klassen --> Besonderheiten - Objektarrays --> Besonderheiten

80

Oder gleiche Wirkung für ein dynamisches Objekt: Beispiel: // gleiche klasse und gleicher Konstruktor wie eben test1 *k2; k2=new test1 (1.5); 2. Eigenschaft: Konstruktoren sind (fast) immer „public“ Beispiel (wenn es nicht so wäre): class test1 { private: float re,im; test1 (float rez) { re=rez; } }; void main(void) { test1 k1(1.5); // compilerfehler: konstruktor // ist nicht verfügbar } 3. Eigenschaft: expliziter und impliziter Aufruf möglich Beispiel: (T) implizit: test1 k1 (1.5); explizit: test1 k2 = test1(1.5); 4. Eigenschaft: Kein Mehrfachaufruf von Konstruktoren Beispiel: // gleiche Klasse test1 k1(1.5); // konstruktor wird gerufen k1.test1(2.0); // Compilerfehler

81

5. Eigenschaft: Verhalten bei dynamischen Objekten Hier gibt es einen wesentlichen Unterschied zwischen den Funktionen „new“ und „malloc“. - mit new-erzeugte Objekte legen das Objekt auf dem Heap tatsächlich an. Der Konstruktor wird aufgerufen. - malloc und calloc reservieren dagegen nur den erforder- lichen Speicherbereich. Der Konstruktor wird nicht aufgerufen. Beispiel: bsp_01.cpp: Konstruktoren beim Aufruf von MALLOC und NEW #include <iostream.h> // oder <iostream> und dann // using namespace std; class test1 { public: test1() { re=3.5; } private: float re,im; }; void main() { test1 k1; test1 *k2=new test1; test1 *k3=(test1 *)malloc (sizeof(test1)); } 6. Eigenschaft: Standardkonstruktor Falls es keinen (überhaupt keinen) Konstruktor gibt, fügt der Compiler einen sogenannten Standardkonstruktor ein. Kennzeichen dieses Standardkonstruktors sind: - leere Anweisungsliste - keine Parameter - keinen Rückgabewert

82

7. Eigenschaft: Konstruktoren können überladen werden Wie bei der Überlagerung von Funktionen, muß sich auch bei der Überlagerung von Konstruktoren die Signatur unterscheiden. Dann kann man eine beliebige Anzahl von Konstruktoren einführen. Beispiel : bsp_02.cpp Überlagerung von Konstruktoren #include <iostream.h>// oder <iostream> und dann // using namespace std; #include <stdlib.h> class test1 { public: test1(float rez) { re=rez;} test1 (float rez,float imz) // unterschiedliche Signatur {re=rez; im=imz;} private: float re,im; }; void main() { test1 k1(2.5); test1 k2(3.5, 4.5); } Sofern auch nur ein Konstruktor definiert wurde, fügt der Compiler keinen Standardkonstruktor mehr ein. Sofern man dann beispielsweise Objekte nicht mehr initialisieren will, muß der Standardkonstruktor vom Programmierer selber eingeführt werden. Beispiel : gleiches wie eben, aber zunächst : test1 k1; // Compilerfehler, da Standardkonstr. fehlt test1() { } // dann Standardkonstruktor definieren und nochmal test1 k1; Bei der Überlagerung muß, wie schon erwähnt, die Signatur ungleich sein. Bei nachfolgendem Beispiel ist dies jedoch nicht der Fall:

83

Beispiel : gleiche Klasse test1(float rez) { } test1(int i) // entsteht Compilerfehler { } ... Lösung: Anwendung des CAST-Operators. 8. Eigenschaft: Objekte in Klassen Enthält eine Klasse ein Objekt einer anderen Klasse, so ruft der äußere Konstruktor immer zu erst die inneren Konstruktoren auf. Dies geschieht vor der ersten Anweisung im äußeren Konstruktor. Beispiel: bsp_03.cpp: gleiche Klasse, wie eben test1 () { } // standardkonstruktor test1 (float rez) { re=rez; } // noch ein Konstruktor class test2 // 2. klasse { public: test2(float rez); { neu=rez; } float neu; test1 k1,k2; // Objekte der Klasse test1. Diese werden aber noch nicht erzeugt. }; ... test2 k3(1.5); ... Initialisierungen von Objekten innerhalb einer Klasse sind analog zu den Aussagen der direkten Datenzuweisung nicht möglich. Folgende Konstruktion ist demnach nicht möglich:

84

Beispiel: gleiches Beispiel wie eben class test2 { ... test1 k1(1.5); ... Durch eine veränderte Syntax wird dieses Problem umgangen. Syntax: äußerer Konstruktor(parameterliste):objektname der inneren Klasse (parameterliste) Beispiel: bsp_04.cpp class test2 { ... test2 (float rez) : k2(1.5); ... } K2 ist dabei ein Objekt der inneren Klasse „test1“. Klassen dürfen keine Objekte der eigenen Klasse besitzen (es denn: Zeiger). Dies ist nur möglich, wenn das eigene Objekt ein Zeiger ist. Nur auf diese Weise ist es möglich, Listen oder rekursive Strukturen zu erzeugen. 10. „Eigenschaft“: Besonderheiten bei der Handhabung von Konstruktoren, falls es sich um Objektarray’s handelt Wird ein Objektarray von einer Klasse erzeugt, gelten bei der Initialisierung der Daten prinzipiell die gleichen Bedingungen, wie bei einer Struktur. Beispiel: gleiche Klasse wie eben test1 k1[3] = { 1.0, 2.0, 3.0 }; // wie bei Struktur ACHTUNG: Die Anzahl der Feldelemente muß aber mit der Anzahl der initialisierten Werte übereinstimmen.

85

Beispiel: test1 k1[3] = {1.0,2.0}; // geht nicht Sofern die Anzahl nicht gleich ist, muß ein Standardkonstruktor vorhanden sein, der dann für das fehlende Arrayelement aufgerufen wird. Der BC-Compiler stürzt sogar für obiges Beispiel ab. Falls bei der Verwendung von Objektarrys mehrere Parameter an einen Konstruktor übergeben werden müssen, führt die implizite Initialisierung nicht zum gewünschten Erfolg. Beispiel: // gleiche Klasse test1 (float rez, float imz) { ... } test1 k2[2]={(1.1,2.1),(2.1,2.2)}; Dies funktioniert analog den Ausagen zum Gleichheitszeichen nicht, d.h. auch hier wird nur der jeweils letzte Klammerwert genommen und der Konstruktor mit einem Parameter aufgerufen. AUSWEG: explizite Initialisierung Beispiel: // gleiches wie eben test1 k2[2]= { test1(1.1,2.1), test1(2.1,2.2) };

8.3.2 Destruktoren

Destruktoren sind das Gegenstück zu Konstruktoren.

Ein abschließendes Beispiel faßt die Aussagen zu Konstruktoren und Destruktoren zusammen.

86

Eigenschaften von Destruktoren

- gleicher Name, wie die Klasse. Diesmal aber mit vorangestellten Tidle (~). - typenlos und ohne Parameterliste - es gibt pro Klasse nur einen Destruktor, d.h. eine Überlagerung ist nicht möglich - Destruktor wird bei Ende der Lebensdauer eines Objektes aufgerufen (z.B. Programmende, Funktionsende) --> auch bei malloc - Hauptanwendung: Aufruf des Destruktors bei dynamischen Objekten. In diesem Fall wird er beim Aufruf der Funktion "delete" ausgeführt - ist kein Destruktor vorhanden, fügt der Compiler einen Standarddestruktor ein, dessen Anweisungsliste leer ist - umgekehrte Reihenfolge bei Objekten in Klassen (erst der äußere, dann die inneren)

87

// bsp_05.cpp // abschliessende gemeinsame uebung zu konstruktoren und destruktoren #include <iostream.h>// oder <iostream> und dann // using namespace std; class klasse1 { public: klasse1(); // konstruktor 1 (prototyp) klasse1(char*); // konstruktor 2 (prototyp) ~klasse1(); // destruktor private: char *text; }; class klasse2 { public: klasse2(); // konstruktor 1 klasse2(int); // konstruktor 2 ~klasse2(); // destruktor private: char *text; klasse1 kl1,kl2,kl3; }; klasse1::klasse1() // konstruktor 1 klasse 1 { cout <<"\nkonstruktor 1: klasse 1"; } klasse1::klasse1(char *austext) // konstruktor 2 klasse 1 { cout <<"\nkonstruktor 2: klasse 1 "; cout <<austext; } klasse1::~klasse1() {cout<<"\ndestruktor klasse 1";} klasse2::klasse2() // konstruktor 1 klasse 2 { cout <<"\nkonstruktor 1: klasse 2"; } klasse2::klasse2(int wert) // konstruktor 2 klasse 2 { cout <<"\nkonstruktor 2: klasse 2 "; cout <<wert; } klasse2::~klasse2() {cout<<"\ndestruktor klasse 2";}

88

void main(void) { klasse1 *k1=new klasse1; klasse1 k2("hallo"); klasse1 *k3=(klasse1*)malloc(sizeof(klasse1)); klasse2 k4; klasse2 *k6=new klasse2(255); delete k3; delete k1; delete k6; } Welche Ausschriften werden durch dieses Programm erzeugt?

89

Ausschriften:

90

8.4 Der Referenztyp in C++

8.4.1 prinzipielle Methoden der Parameterübergabe

Das klassische C hat im Vergleich zu anderen Sprachen im Hinblick der Parameterübergabe einen entscheidenden Nachteilt. Während es fast überall möglich ist, an eine Prozedur oder Funktion die Pararmeter in der Form call by value bzw. call by reference zu übergeben, sieht das klassische C nur die erste Variante vor. Damit war die direkte Manipulation von Variablen der aufrufenden Funktion nicht möglich. Man half sich in C damit, in dem man einen Umweg beschritten hat und auf diese Weise quasi ein call_by_reference erzeugen konnte. Hierzu wurden Zeiger- und Adreßoperationen eingesetzt. Die Effektivität dieser Methode ist aber einem tatsächlichen „call by reference“ in etwa gleichzusetzen. Die umständliche Schreibweise (*) bleibt aber bestehen. Ein typisches Beispiel zur Veranschaulichung der verschiedenen Parameterübergabeformen stellt die Vertauschung von zwei Variableninhalten dar, wobei es das Ziel ist, die Werte der Ausgangsvariablen, d.h. der Variablen der aufrufenden Funktion, zu vertauschen. Nachstehendes Beispiel zeigt beide Formen der Parameterübergabe im klassischen C: Beispiel: (nächste Seite) In C++ ist nun das „übliche“ call_by_reference implementiert. Als Referenzoperator dient erneut der Operator: & , der also wiederum überlagert worden ist. Gleichfalls führt die Einführung dieses Operators als Referenz einer Variable sehr oft zu Verwirrungen, da er in entsprechenden Quelltexten sehr oft mit dem Adreßoperator verwechselt wird. In diesem Zusammenhang ist bei der Analyse und bei eigenen Implementierungen also Vorsicht geboten.

91

mögliche Parameterübergaben in C (call_by_reference fehlt) #include <iostream.h> int wert1,wert2; // integervariablen void tausch_by_value (int, int); // call_by_value void tausch_by_umweg (int *, int *); // adressübergabe stellt // umweg dar void tausch_by_value(int _wert1, int _wert2) { int zw; // zwischenwert zw = _wert1; _wert1 = _wert2; _wert2 = zw; // angeblicher tausch // der ausgangs- } // variablen void tausch_by_umweg(int *_wert1, int *_wert2) { int zw; // zwischenwert zw = *_wert1; // schreibweise *_wert1 = *_wert2; // auch lästig *_wert2 = zw; // erfolgreiches // vertauschen der } // ausgangsvariablen void main(void) { wert1=10; wert2=20; tausch_by_value(wert1,wert2); // wertübergabe cout <<"\nwert1: " << wert1 << " wert2: " << wert2; tausch_by_umweg(&wert1,&wert2); cout <<"\nwert1: " << wert1 << " wert2: " << wert2; }

92

Syntax der Referenzmethode (Referenzvariable): typ &<identifikator> = initialisierung; Beispiel: ... int wert1, wert2; // normale variable int & auchwert1 = wert1; // auchwert1 ist referenz für wert1 int & auchwert2 = wert2; // auchwert2 ist referenz für wert2 Referenz bedeutet, daß die Referenzvariable ein Aliasname für eine feste Variable (für einen Speicherplatz) wird. Während bei der Übergabe mittels call_by_value eine lokale Kopie des Übergabeparameters angelegt wird (also zwei Variable und zwei Werte sofern ein Parameter übergeben wird), handelt es sich bei der Referenzübergabe um eine Variable mit zwei Namen. Referenz bedeutet dabei, daß eine Referenzvariable eine Referenz für einen ganz bestimmten Speicherplatz darstellt. Das heißt aber auch, daß eine Referenz nur einmal festgelegt werden kann und nicht wie bei Zeigern innerhalb des Programms verändert werden darf. Beispiel: bsp_06.cpp #include <iostream.h> int wert1=10,wert2=20, &_wert1=wert1; // referenz int *wert11, *wert22; // zeiger void main(void) { wert11=&wert1; wert22=&wert2; // (im prinzip jetzt referenz _wert1=wert2; // zwar nimmt wert1 den wert von wert2 // an, aber auch wert1 verändert sich, d.h // im gegensatz zu zeigern: einmal // referenz, immer referenz wert11=wert22; // wechsel der adresse *wert11=30; // alte referenz besteht nicht mehr } // jetzt OVERHEAD Wichtig: Referenzvariable sind bei der Deklaration zu initialisieren (welche Referenz soll denn erzeugt werden). " Ein Referenzparameter in der Argumentenliste einer Funktion wird vom Compiler so verstanden, als ob er mit dem übergebenen Argument initialisiert wird".

93

// bsp_07.cpp: call_by_reference ist erst in C++ möglich #include <iostream.h> int wert1,wert2; // normale integervariablen void tausch_by_reference (int &, int &); // call_by_reference void tausch_by_reference(int & auchwert1, int & auchwert2) { int zw; // zwischenwert zw = auchwert1; auchwert1 = auchwert2; auchwert2 = zw; // erfolgreiches // vertauschen der // ausgangsvariablen cout << "\nadr. wert1: " << &wert1 << "adr. auchwert1: " ; cout << &auchwert1; } void main(void) { wert1=10; wert2=20; cout << "\n\nvor dem aufruf der tauschfunktion"; cout << "\nwert1: " << wert1 << " wert2: " << wert2; tausch_by_reference(wert1,wert2); // referenzübergabe // & auchwert1 wird mit // wert1 initialisiert und // & auchwert2 mit wert2 cout << "\n\nnach dem aufruf der form 'call_by_reference ' "; cout << "\nwert1: " << wert1 << " wert2: " << wert2; } // OVERHEAD

94

8.4.2 Besonderheiten der Referenzmethode

Bei der Verwendung der Referenzmethode in C++ sind einige Besonderheiten zu beachten, die mit der uner C nicht immer strengen Typkontrolle in Zusammenhang stehen. Zur Veranschaulichung dient ein aus /2/ entnommenes Beispiel. Beispiel: bsp_08.cpp

Allgemeines zum Beispiel: - Die Funktion "byref(...)" bekommt eine Referenz auf einen Integerwert übergeben. - Von der "main-Funktion" werden verschiedene Übergabeparameter getestet 1. Variante: - Übergabe eines Arrayelementes ( wert[4] ), das vom Typ "int" ist - rx ist eine Referenz von "wert[4]" und ändert damit in "byref(..)" den wert dieses Speicherplatzes 2. Variante: - sinngemäß gleiche Aussagen wie zur Variante 1 3. Variante: - Übergabe einer Konstante. Dies würde aber zu einem syntaktischen Fehler werden, falls rx durch den Wert 100 ersetzt würde ( die Anweisung 100++ geht nicht ). - C++ erstellt hierfür eine temporäre Variable vom gleichen Typ der Referenz, also hier Integer. Diese Variable wird auch als anonym bezeichnet. 4. Variante: - sinngemäß gleiche Aussage wie für Variante 3 5. Variante: - Übergabe eines falschen Types ( long) - C++ legt ebenfalls eine anonyme Variable vom Typ "int" an, wodurch die Variable "long ing" nicht verändert wird.

95

// bsp_08.cpp: (entnommen aus /2/) - verschiedene Referenzargumente #include <iostream.h> void byref(int & rx); // rx ist eine Referenz void byref(int & rx) { for(int i = 0; i < 3; i++) cout << rx++ << "... "; cout << "\n"; } int main(void) { int werte[5] = {2,4,6,8,10}; // normales int-Feld struct teile { int a; float b; } ding = { 600, 3.14 }; // normale struct-init int x = 20; byref(werte[4]); // 1. Variante cout << "werte[4] = " << werte[4]; cout << " nach Verwendung von byref()\n"; byref(ding.a); // 2. Variante cout << "ding.a = " << ding.a; cout << " nach Verwendung von byref()\n"; byref(101); // 3. Variante byref(2 * x + 3); // 4. Variante cout << "x = " << x << " nach Verwendung von byref()\n"; long ing = 15; byref(ing); // 5. Variante cout << "ing = " << ing << " nach Verwendung von byref()\n"; }

96

Programmausschriften zum Beispiel

bsp_08.cpp

1. Variante 10... 11... 12... werte[4] = 13 nach Verwendung von byref() 2. Variante 600... 601... 602... ding.a = 603 nach Verwendung von byref() 3. Variante 101... 102... 103... 4. Variante 43... 44... 45... x = 20 nach Verwendung von byref() 5. Variante 15... 16... 17... ing = 15 nach Verwendung von byref()

97

8.4.3 Referenzen und Objekte

a) Referenzobjekte und konstante Referenzobjekte Die Syntax ist analog den bisherigen Betrachtungen aufgebaut. Beispiel: bsp_09.cpp Wie aus dem Beispiel zu ersehen ist, ist es durch die Referenzübergabe möglich, die Objektdaten zu verändern, einschließlich der privaten. Das kann aber bei der Programmierung sehr gefährlich werden. Aus diesem Grund wird bei der Übergabe einer Referenz sehr häufig der Typqualifizierer const hinzugefügt. Da bekanntermaßen Konstanten nicht verändert werden können, ist damit ein ausreichender Schreibschutz gewährleistet. Sollte man denken. Beispiel: immer noch bsp_09.cpp class test1 {...} f1 (const test1 &ref) {...} test1 k1(1.5),k2(2.5); f1(k2); // an dieser Stelle entsteht ein Compilererror und // eine WARNUNG, mit dem Hinweis, daß // es eigentlich ein ERROR ist Beides einzeln erzeugen (Error und Warnung, durch Kommentarvorsetzung in f1) ERROR: direkter Schreibversuch auf die public-Variable imz WARNUNG: indirekter Schreibversuch über eine Member- funktion auf eine private Variable D.H. Es ist eher möglich über eine Memberfunktion private zu verändern, als public-Elemente auf direktem Wege. Es gibt Compiler, die letzteres nicht nur als Warnung ausgeben.

98

// bsp_09.cpp: referenzen auf objekte (uebergabeparamter) #include <iostream.h> class test1 { public: float imz; test1(float rez) // konstruktor { re=rez; imz=10.0; } void set() { re=30; } // setzen einer privaten // variablen private: float re,im; // realteil, imaginärteil }; void f1(test1 &ref) // referenzübergabe { cout <<"\n"<<ref.imz; // lesezugriff ref.imz=3.0; // schreibzugriff auf public ref.set(); // schreibzugriff auf private } void main(void) { test1 k1(1.5); test1 k2(2.5); f1(k2); // k2 ist anschließend // verändert } // jetzt OVERHEAD

99

AUSWEG: Deklaration der Memberfunktion als "const". Syntax: <typ> <name der funktion> (parameterliste) const Beispiel: void set() const ... ABER: Nun kann man "set" vergessen, da keine Variablen verändert werden dürfen!! b) Referenzen als Klassenmitglieder Natürlich können Klassen auch Referenzen als Mitglieder enthalten. Wie bereits bekannt ist, müssen Referenzen bei ihrer Erzeugung initialisiert werden. Gleichfalls ist aber auch bekannt, daß Klassenmitglieder nicht automatisch, sondern nur über Konstruktoren initialisiert werden können. Folgende Konstruktionen würde deshalb zu einem Compilerfehler führen: Beispiel: class test1 { private: int wert, &_wert1; // fehlende Init oder private: int wert, &_wert1=wert; // klasseninit verboten } Auch in diesem Fall stellt ein spezielle Syntax, ähnlich wie bei Objekten als Klassenmitglieder, wieder eine Lösung dar. Die entsprechenden Konstruktoren sind wiederum über den Doppelpunktoperator zu verändern. Allgemeine Syntax: konstruktor(...) : referenzvariable (welche referenz) Beispiel: bsp_10.cpp

// referenzen als klassenmitglieder (lösung des init-problems)

100

#include <iostream.h> class test1 { public: test1():_wert1(wert) // init referenz { } test1(float rez):_wert1(wert) // init referenz { re=rez; } private: float re,im; // realteil, imaginärteil int wert, &_wert1; }; void main(void) { test1 k1; } c) Referenzen als Rückgabewert einer Funktion Referenzen können genau wie andere Datentypen als Rückgabewert einer Funktion dienen. Zu beachten ist jedoch, daß eine Rückgabe nicht auf lokale Variablen erfolgen kann, da diese ja nach Beendigung der Funktion ihre Lebensdauer verlieren. Beispiel: float &f1(void) { float re; re=20.2; return re; // geht nicht: lokale variable } Rückgabe einer Referenz ist nur auf globale (also auch statische) Varaiblen möglich. Beispiel : gleiches wie eben, jetzt aber: static float re; Selbstverständlich können nun auch Referenzen auf Objekte zurückgegeben werden, sofern das eben Erläuterte eingehalten wird. Gezeigt werden soll dies an einem Beispiel, bei dem ein Zielobjekt in Abhängigkeit eines Ereignisses die Kopie eines anderen Objektes enthält. Beispiel: Referenzen als Rückgabewert

101

// bsp_11.cpp: referenzen als rueckgabewert (referenz auf objekt) #include <iostream.h> class test1 { public: test1() {} test1(float rez) { re=rez; } private: float re,im; // realteil, imaginärteil }; test1 &f1(int i) // rückgabe einer objektreferenz { static test1 k2(2.5); // was passiert, wenn static fehlt static test1 k3(3.5); if(i==1) return k2; else return k3; } void main(void) { test1 k1,k4; // k4 ist zielobjekt k4 = f1(2); // k3 wird zurückgeben }

102

8.5 Freundfunktionen und Freundklassen

8.5.1 Freundfunktionen

Freundfunktionen sind Funktionen, die keine Methode einer Klasse darstellen, aber trotzdem auf alle Komponenten einer oder mehrerer Klassen zugreifen können. Zur Kennzeichnung von Freundfunktionen dient das Schlüsselwort:

friend Der Hauptanwendungsfall der Einführung derartiger Freundfunktionen ergibt sich dann, wenn eine Funktion auf mehrere Klassen zugreifen soll. Man kann die gleiche Funktion nicht in mehreren Klassen definieren, sofern auf private Daten dieser Klassen zugegriffen werden muß. Während Elementefunktionen durch den Aufruf eines spezifizierten Objektes aufgerufen werden und somit eine eindeutige Zuordnung existiert, müssen den Freundfunktionen mitgeteilt werden, mit welchem Objekt bzw. mit welcher Klasse sie arbeiten sollen. Dazu muß den Freundfunktionen als Parameter das zugehörige Objekt und die Klasse übergeben werden. Syntax : class <name> { private: ... friend <typ> <fkt.-name> (<name der klasse> objekt,...); ... }; <typ> <fkt.name> (<name der klasse> objekt,...) { <anweisungsliste>; } Durch diese Deklaration kann die Freundfunktion auf alle Komponenten, also auch auf die privaten einer Klasse zugreifen. Beachtet werden muß, daß nur im Prototyp des Schlüsselwort „friend“ anzugeben ist, nicht jedoch bei der Definition der Freundfunktion. Weiterhin entfällt die Zuordnung der Freundfunktion zu einer Klasse mittels „ :: “- Operators, da diese Funktion keine Memberfunktion darstellt. Damit ist gleichfalls eine Inlinedefinition ausgeschlossen, d.h. die Definition erfolgt immer außerhalb der Klassen.

103

Beispiel: #include <iostream.h> class test1 { public: test1(); // 1. konstruktor ohne param. friend void erg (test1 k1); // prototyp friend-funktion private: float re,im; // realteil, imaginärteil }; // ende der klasse void erg (test1 k1) // ohne „friend“-Schlüssel { cout << k1.re; // lesezugriff auf private k1.im=23.3; // schreibzugriff } void main(void) { test1 k0; // k0 erzeugen,ersten // konstruktor ausführen erg (k0); // aufruf der freundfunktion } Mit dieser Variante einer Freundfunktion kann also hemmungslos auf die Daten einer Klasse zugegriffen werden. Dies ist aber nicht im Sinne der objektorientierten Programmierung, die sich ja auf die Fahne geschrieben hat, etwas für die Datensicherheit zu tun. Aus diesem Grund kann auch für Freundfunktionen der Typ const hinzugefügt werden. Beispiel: wie eben aber erg(const test1 k1) Damit entsteht durch den Schreibzugriff schon ein Compilerfehler. Muß man aus irgendwelchen Gründen eine Kopie des Objektes anfertigen, kann dies programmtechnisch sehr aufwendig werden. Eine Möglichkeit dies zu vermeiden, stellt die Referenzmethode dar. In diesem Fall wird der Freundfunktion (oder auch einer anderen) das Objekt als Referenz übergeben, wodurch ja bekanntermaßen ein Alias erzeugt wird.

104

Syntax (unter Berücksichtigung der Referenzmethode): class <name> { ...

friend <typ> <fkt.-name> (<name der klasse> & objekt,...);

... };

<typ> <fkt.name> (<name der klasse> & objekt,...)

{ <anweisungsliste>; } Mit dieser Variante ist das Erstellen einer Kopie zwar sehr einfach gelöst, aber es ist immer noch denkbar, daß Komponenten des Objektes versehentlich oder gar beabsichtigt zerstört oder verändert werden. Um dies zu vermeiden, kann wiederum der Typspezifier „ const “ in der Parameterliste hinzugefügt werden. Syntax (unter Berücksichtigung der Übergabe einer Referenz als Konstante) : class <name> { ... friend <typ> <fkt.-name> ( const <name der klasse> objekt,...); ... }; <typ> <fkt.name> (const <name der klasse> objekt,...) { <anweisungsliste>; } Beispiel: bsp_12.cpp

105

// bsp_12.cpp Nutzung von Freundfunktionen #include <iostream.h> class werkzeug; // wegen der freundfunktion // es existiert eine Vorwärts- // referenz. deshalb muß unvollst. // deklaration erfolgen class stuhl { public: stuhl(int geld) { preis=geld;} friend gesamt (const stuhl &, const werkzeug & ); // nur prototyp private: int preis; }; class werkzeug { public: werkzeug (int auchgeld) { preis=auchgeld; } friend gesamt (const stuhl &, const werkzeug &); // nur prototyp private: int preis; }; gesamt (const stuhl &s1, const werkzeug &w1) // ohne friend { return (s1.preis+w1.preis); } void main() { stuhl s1(120); werkzeug w1(150); cout <<"\nder gesamtpreis betr„gt :" << gesamt(s1,w1); }

106

8.5.2 Freundklassen

Klassen können in C++ ebenfalls als Freund einer anderen Klasse deklariert werden. Alle Komponenten der Freundklasse (Daten und Methoden) sind dann Freunde der anderen Klasse und können von ihr benutzt werden. Bei der Benutzung von Freundklassen ist prinzipiell zu berücksichtigen, daß der Compiler „von vorn nach hinten“ übersetzt. Die Freundklasse muß demzufolge als Prototyp vereinbart werden.

Beispiel: class klass1; // prototyp der freundklasse vereinbaren. class klass2 { friend class klass1; // compiler kennt klass1 schon } class klass1 { ... } Wichtig: Auch beim Zugriff mit Freundklassen muß das Objekt als Parameter übergeben welchen, auf dessen Daten als Freund zugegriffen werden soll. Beispiel: bsp_13.cpp

107

// bsp_13.cpp: Freundklassen #include <iostream.h> class konto1; // unvollst. deklaration wg. vorwärtsref. class konto2 { public: friend class konto1; // damit kann konto1 alles konto2(int mehr) { ein2=mehr;} erg2() {cout <<"\neinnahmen konto2 "<<ein2; cout <<"\nausgaben konto2 "<<aus2;} ~konto2() {cout<<"\nkonto2 schliessen\n";} private: int ein2,aus2; }; class konto1 { public: konto1(int mehr) { ein1=mehr;} klau(konto2 &); // beim zugriff muß aber beachtete // werden, mit welchem objekt auf // konto 2 zugegriffen werden soll erg1() {cout <<"\neinnahmen konto1 "<<ein1; cout <<"\nausgaben konto1 "<<aus1;} ~konto1() {cout<<"\nkonto1 schließen\n";} private: int ein1,aus1; }; konto1::klau(konto2 & t1) { t1.aus2=150; // auf konto1 zugreifen ein1=ein1+t1.aus2; // eigene gutschrift aus1=0; t1.erg2(); // durch friendklasse alles zugaenglich erg1(); } void main(void) { konto1 e1(100); konto2 e2(1000); e1.klau(e2); //zugriff als freundklasse e2.erg2(); }

108

8.6 Operatorüberlagerung (Zusatzinformation, kein Bestandteil in Prog. II)

Eine Form des Polymorphismus in der OOP stellt neben der Möglichkeit der Überlagerung von Funktionen auch die Mehrfachverwendung von Operatoren dar. Auf diese Weise lassen sich insbesondere Objekte einfach bearbeiten. Bereits überlagerte Operatoren in C sind z. B. das ‘ * ‘, das ‘ - ‘ usw.

8.6.1 einfache Operatorüberlagerung

Syntax zur Überlagerung von Operatoren: <funktionstyp> operator <welcher operator> (argumentenliste); operator: Schlüsselwort welcher : z. B. ‘ + ‘ liste : z. B. mit welchem Datentyp Diese Syntax ist analog einer Funktionsdeklaration aufgebaut.

Wichtig:

Operatorüberlagerungen werden in C++ durch Funktionen realisiert. Zunächst werden ein paar Regeln im Hinblick der Möglichkeiten zur Operatorüberlagerung behandelt. Wichtige Merkmale zur Operatorüberlagerung (nächste Seite): 1. Eigenschaft: ( nur vorhandene Operatoren sind überlagbar) Beispiel : bsp_14.cpp (nur syntaktische Erläuterung, also nur Übersetzung) #include <iostream.h> class optest { public : int operator $ (int w) // unzulaessiger operator { wert=w; return wert;} private: int wert; }; void main(void) { optest op1; }

109

Regeln zur Operatorüberlagerung - es können nur Operatoren überlagert werden, die bereits existieren. - nicht redefinierbare Operatoren sind: . , :: , ?: , # , ## - Funktionen zur Operatorüberlagerung erwarten (im Allgemeinen) einen Bezug zu selbst definierten Daten- typen - Priorität der Operatoren kann nicht verändert werden - bei der Überlagerung der Increment ( ++ )- und Dekrement( -- )operatoren kann anschließend nicht mehr zwischen Präfix- und Postfiximplementierung unterschieden werden ( ++var ist dann identisch mit var++) - sofern ein Operator sowohl einstellig als auch zweistellig auftreten kann (z. B. ‘ - ’Operator) kann dieser auch getrennt überlagert werden. - folgende Operatoren sind nur durch Klassenmethoden redefinierbar = , [ ] , ( ) , ->

110

2. Eigenschaft: (einige Operatoren dürfen nicht überlagert werden ) Beispiel: z. B. '::'- oder '?:'- Operator 3. Eigenschaft: (Operatorfunktionen erwarten Bezug zu selbst definierten Datentypen (struct, class) ) Die übrigen Eigenschaften werden an dieser Stelle zunächst nicht näher erläutert. Anhand eines ersten Beispieles soll die Operatorüberlagerung hinsichtlich der Syntaktik veranschaulicht werden. Beispiel: bsp_15.cpp (Überlagerung des +-Operators und des Incrementoperators) #include <iostream.h> class test1 { public: test1(int w1) // konstruktor {wert1=w1;} int operator +(int wert2) // +-Operator { if(wert2<=wert1)return wert1; // es kann alles mögliche return(wert1+wert2); // realisiert werden } int operator ++() // incrementoperator { wert1++; } // warnung wegen postfix und // präfix noch nicht berücksichtigen private:int wert1; };

111

void main(void) { test1 t1(20); // erzeugung des objektes erg=t1+30; // aufruf der überlagerungs- // routine, da es sich um ein // objekt handelt t1++; // dito ++t1; // kein unterschied zwischen // präfix und postfix } // jetzt OVERHEAD Nachdem nun ein erstes Beispiel zur Syntax gezeigt wurde, soll der Hintergrund dieser Syntax einmal veranschaulicht werden. Sofern der +-Operator auf ein Objekt einer Klasse angewendet wird, versucht der Compiler folgendes zu machen:

Beispiel: Compilerversuch bei Überlagerung des +-Operators

Frage: Geht demnach folgende Anweisung? erg= 30+ t1; // nur vertauscht --> NEIN Wichtig! Sofern die Operatorfunktion eine Memberfunktion ist, muß an erster Stelle immer ein Objekt stehen. Was kann man nnun tun, damit auch die Umkehrung der Operanden ermöglicht werden kann? --> Einsatz von Freundfunktionen, denn Freundfunktionen müssen nicht mit einem Objekt aufgerufen werden. Beispiel: immer noch bsp_15.cpp // gleicher Aufbau wie eben // aber Erweiterung der Klasse friend int operator + (int , test1 &); // prototyp ... // außerhalb der Klasse int operator +( int wert2, test1 & t) { if(wert2<=t.wert1)return t.wert1 return (t.wert1+wert2); } ... erg=30+t1; // jetzt möglich

112

Compilerkonvertierung bei Anwendung des ‘ + ‘ - Operators

auf Objekte einer Klasse

Anweisung in C++

daraus versucht der Compiler zu machen:

erg

erg

t1 30 =

=

+

t1.operator + (30)

Aufruf einer Methode „operator +“ durch das

Objekt t1

mit dem Übergabe- parameter 30 (call by value)

113

8.6.2 Überladen von Operatoren mit unterschiedlichen Bedeutungen

Das klassische Beispiel hierfür stellt der Minusoperator dar. Er ist sowohl mit zwei Argumenten (Subtraktionsoperator) als auch mit einem Argument (Zweierkomplement) einsetzbar. Auch hierfür wieder ein Beispiel: Beispiel: bsp_16.cpp #include <iostream.h> int wert1,erg; class test1 { public: test1(int w1) // konstruktor {wert1=w1;} int operator -(int wert2) // Subtraktionsoperator { // es wird ein Argument // übergeben. Der 1. Operand // ist wieder das Objekt if(wert2>=wert1)return wert1; return(wert1-wert2); } int operator -() // Zweierkomplement { wert1=-wert1; } private: int wert1; }; void main(void) { test t1(20); erg=t1-30; // Subtraktionsoperator t1=-t1; // erst Zweierkomplement- // funktion, dann Aufruf des } // Konstruktors // jetzt OVERHEAD Mittels Operatorfunktionen lassen sich Objekte neu initialisieren, d.h. ein mehrfacher Aufruf von, auch unterschiedlichen, Konstruktoren ist auf diese Weise möglich.

114

8.6.3 Operatorüberlagerung zur Behandlung von Objekten

Das eine Operatorüberlagerung insbesondere für selbstdefinierte Datentypen sinnvoll ist, soll das nächste Beispiel veranschaulichen. Beispiel: Addition zweier komplexer Zahlen Es sollen zwei komplexe Zahlen addiert werden. Jede komplexe Zahl wird durch je ein Objekt, bestehend aus Real- und Imaginärteil, dargestellt. Das Ergebnis soll durch ein drittes Objekt (dritte komplexe Zahl) repräsentiert werden. Wie könnte dieses Problem auf herkömmliche Weise programmtechnisch realisiert werden? Beispiel: programmtechnische Realisierung (nur zur Kenntnis) Wenn man bedenkt, daß ein Ziel der OOP darin bestand, sich nicht um den Aufbau eines Objektes, sondern nur um dessen Handhabung zu kümmern, so kann ein Listing zur Addition zweier komplexer Zahlen nach diesem Beispiel nicht akzeptiert werden. Analog den eben erfolgten Betrachtungen wäre eine Anweisung add = k1 + k2; wünschenswert. Die Verwaltung dieser Anweisung muß natürlich die Klasse "komplex" übernehmen. Was mit Konstanten geht, funktioniert natürlich auch innerhalb von Objekten. Was würde wiederum der Compiler versuchen, wenn im Quelltext die Anweisung add = k1 + k2; erscheinen würde? Beispiel: Compilerversuch zur Überlagerung von Operatoren. bei Objekten Wie könnte nun eine akzeptable Lösung aussehen? Beispiel: bsp_17.cpp Beispiel Operatorüberlagerung mit Objekten

Es ist festzustellen, daß in der vorliegenden Implementierung der Rückgabetyp der Überlagerungsmethode vom Typ der Klasse sein muß, um in der Anweisung add = k1 + k2 keine Typverletzung zu erzielen. ("Auszug aus complex.h " )

115

// beispiel zeigt nicht sehr anspruchsvolle möglichkeit der addition // zweier komplexer zahlen #include <iostream.h> class komplex { public: komplex() { } // 1. konstruktor komplex(float rez, float imz) // 2. konstruktor { re=rez; im=imz; } private: float re,im; // realteil, imaginärteil // ausgabe des ergebnisses. dazu werden beide objekte übergeben void erg(const komplex & k1, const komplex & k2) { re = k1.re + k2.re; im = k1.im+ k2.im; cout << "\n resum : " <<re <<"\n imsum : " <<im <<"\n"; } }; // klassenende void main(void) { komplex k1(1,3); // erste komplexe zahl komplex k2(2,5); // zweite komplexe zahl komplex add; // ergebnis der addition add.erg(k1,k2); // aufruf der // ergebnisfunktion }

116

Compilerkonvertierung bei Anwendung des ‘ + ‘ - Operators

auf Objekte einer Klasse

Anweisung in C++

daraus versucht der Compiler zu machen:

add

add

k1 k2 =

=

+

k1.operator + (k2)

Aufruf einer Methode „operator +“ durch das

Objekt k1

mit dem Übergabe- parameter k2

(objekt)

117

// bsp_17.cpp operatorueberladung fuer objekte #include <iostream.h> class komplex // klasse komplex { public: (1) komplex() { } // standardkonstruktor (2) komplex(float rez, float imz) // 2. konstruktor (zur init) { re=rez; im=imz; } (3) komplex operator+(komplex &b); void erg () { cout <<"\n realsumme : " <<re; // ausgabemethode cout <<"\n imagsumme : " <<im <<"\n"; } private: float re,im; // realteil, imaginärteil }; // ende der klasse (4) komplex komplex::operator+ (komplex & ko2) // wie ein member { (5) float sre,sim; (6) sre = re + ko2.re; // re --> k1.re, ko2.re --> k2.re (7) sim = im + ko2.im; // dito fuer imaginaer (8) return komplex (sre,sim); // 2. konstruktor fuer ‘add’ } void main(void) { (9) komplex k1(1,3); komplex k2(2,5); // beide kompl. zahlen erz. (10) komplex add; // ergebniszahl (11) add = k1 + k2; // neuer konstruktor. (12) add.erg(); // ausgabe des ergebnisses

} // ende

118

Auszug aus " complex.h" // Binary Operator Functions friend complex _Cdecl operator+(complex &, complex &); friend complex _Cdecl operator+(double, complex &); friend complex _Cdecl operator+(complex &, double); friend complex _Cdecl operator-(complex &, complex &); friend complex _Cdecl operator-(double, complex &); friend complex _Cdecl operator-(complex &, double); friend complex _Cdecl operator*(complex &, complex &); friend complex _Cdecl operator*(complex &, double); friend complex _Cdecl operator*(double, complex &); friend complex _Cdecl operator/(complex &, complex &); friend complex _Cdecl operator/(complex &, double); friend complex _Cdecl operator/(double, complex &); friend int _Cdecl operator==(complex &, complex &); friend int _Cdecl operator!=(complex &, complex &); complex & _Cdecl operator+=(complex &); complex & _Cdecl operator+=(double); complex & _Cdecl operator-=(complex &); complex & _Cdecl operator-=(double); complex & _Cdecl operator*=(complex &); complex & _Cdecl operator*=(double); complex & _Cdecl operator/=(complex &); complex & _Cdecl operator/=(double); complex _Cdecl operator+(); complex _Cdecl operator-();

119

8.6.4 Überlagerung des Increment- bzw. Dekrementoperators

Auch diese beiden Operatoren (++, --) können überlagert werden. Bekanntermaßen lassen sich beide in sogenannter Präfix- bzw. Postfixnotation verwenden. Beispiel: int a,b,c; ... a=b=c=10; ... b=a++; // identisch mit: b = a; a = a+1; --> Postfix b=++a; // identisch mit: a = a+1; b = a; --> Präfix Bei der Präfixnotation wird erst der Operator angewendet und danach erfolgt die Zuweisung, während es bei der Postfixart genau umgekehrt ist. In einigen Literaturstellen wird fälschlicherweise behauptet, daß nach der Überlagerung o. g. Operatoren keine Unterscheidung zwischen Präfix- und Postfixnotation mehr vorgenommen werden kann. Diese Aussage bezieht sich auf ältere Compiler und ist nicht mehr aktuell. Statt dessen hat man eine Vereinbarung getroffen, die die übliche Handhabung unterstützt. Syntax (Präfix): ... operator ++ () ... ... Syntax (Postfix): ... operator ++(int) ... ... Bei der zweiten Variante wird als Integerwert prinzipiell der Wert 0 übergeben. Analoge Aussagen treffen auf den Decrementoperator zu. Beispiel: bsp_18.cpp

120

// bsp_18.cpp // beispiel zeigt anwendung zur überlagerung des incrementoperators, // wobei präfix- und postfixnotationen implementiert sind include <iostream.h> class test1 // klasse test1 { public: test1(float rez, float imz) // konstruktor (zur init) { re=rez; im=imz; } void operator++ ( ); // präfix void operator++ ( int ); // postfix void erg () { cout <<"\nder realteil beträgt: " << re; } private: float re,im; }; // ende der klasse void test1::operator ++() { re=re+1; cout <<"\npraefix: der realteil ist: "<<re; } void test1::operator ++(int) { cout <<"\npostfix: der realteil ist: "<<re; re=re+1; } void main(void) { test1 k1(1,3); k1.erg(); // vor der anwendung des op. k1++; // postfix ++k1; // präfix test1 k2(1,3); k2.erg(); // gleiche zahlen ++k2; // präfix k2++; // postfix } // jetzt OVERHEAD Programmausschriften zu bsp_18.cpp:

121

der realteil beträgt: 1 postfix: der realteil ist: 1 praefix: der realteil ist: 3 der realteil beträgt: 1 praefix: der realteil ist: 2 postfix: der realteil ist: 2

8.6.5 klassengebundene Operatorüberlagerung (Zusatzinformation)

Während eine Vielzahl von Operatoren sowohl durch Klassenmethoden, als auch durch andere Funktionen überlagert werden können, gibt es eine Reihe von Operatoren, die ausschließlich durch Memberfunktionen redefinierbar sind. Zu dieser Gruppe gehört auch der Indexoperator . [ ] Eine der Hauptnachteile von C stellt die Nichtüberprüfung von Indexgrenzen dar. Dadurch kann es zu erheblichem Datensalat kommen. Beispiel: int a[20],i,j,k; ... for (i=0;i<30;i++) a[i]=i; // Überschreitung der Feldgrenzen ... In C++ ist dieser Nachteil beseitigt, allerdings muß man etwas dafür tun. Eine Lösung besteht in der Überlagerung des Indesxoperators, eine zweite in der Anwendung einer bereits existierenden Klasse ( deklariert in „array.h“, aber nicht bei allen C++ - Compilern). Im nachfolgenden Programm wird eine Lösung zur Indexüberwachung gezeigt. Gleichfalls ist demonstriert, wie C++ die jeweils richtige Auswahl für eine Operatormethode wählt (Standarddatentyp oder selbstdefinierter).

Beispiel: bsp_19.cpp

122

// bsp_19.cpp programm zeigt die ueberlagerung // des indexoperators um arrayueberlauefe abzufangen. demonstriert wird hier // nur der lesezugriff #include <iostream.h> #include <stdlib.h> // zur reservierung von speicher class _array // eine arrayklasse aufbauen { private: int *mem,*stelle; // memory fuer das array, zur // veranschaulichung anhand ueber- // lagerter speicherplätze int _arraylaenge; // laenge des feldes public: _array(int laenge); // konstruktor int _array:: operator [ ] (int indexwert); // prototyp zur ueberlagerung ~_array(void) // destruktor { free(mem); free(stelle); // reservierten speicher freigeben } }; // ende der klasse _array :: _array(int laenge) // maximale laenge des feldes // konstruktor { int i; // dynamischen speicher reservieren stelle=mem=(int *)calloc(laenge,sizeof(int)); for(i=0;i<laenge;i++) *stelle++ = i+10; // speicherinhalt schreiben, dient als // spaetere ueberpruefung stelle=mem; // wieder startadresse des arrays _arraylaenge=laenge; // arraylaenge merken }

123

// programmfortsetzung int _array::operator [ ] (int indexwert) // überlagerung [ ] { if (indexwert < _arraylaenge) { stelle=mem+indexwert; // speicherstelle auswaehlen return (*stelle); // inhalt zurueckgeben } cout <<" indexueberlauf, "; // indexüberlauf cout <<"program terminated\n"; exit(1); //destruktor, programm beenden // finden Sie eine bessere Möglichkeit // denn "exit" ist verboten

return(0); // vermeidung einer warnung } void main(void) { int wert[3],i; // hierbei wird der [ ]-op. nicht // ueberladen, da er auf einen // standardtyp angewandt wird _array feld1(3); // feld1 als dynamisches // array von 0..2 definieren for (i=0;i<3;i++) wert[i]=feld1[i]; // speicherplatz, der im konstruktor // initialisiert wurde, lesen. // wert[i] erfolgt ohne ueberlagerung, // feld1[i] dagegen mit for (i=0;i<5;i++) // ausgabe ohne indexueberwachung { cout <<"\nindex= " <<i; cout <<" wert = " <<wert[i]; // ab i==3 werden indexgrenzen } // verletzt. Aber keine meldung cout <<"\n\n\n"; for (i=0;i<5;i++) // ausgabe mit indexueberwachung { cout <<"\nindex= " <<i; cout <<" feld1 = " <<feld1[i]; // beim erstmaligen auftreten eines } // indexueberlaufes erfolgt // programmabbruch } // diese stelle wird nie erreicht

124

Als Nebeneffekt entsteht ein dynamisches Feld, d.h. die Felddimension kann zur Laufzeit eingegeben werden.

8.6.6 Typumwandlung mit Operatorfunktionen (Zusatzinformation)

C und C++ führen fast in allen Fällen automatisch Typumwandlungen durch. Beispiel: int a = 7.3; // a wird 7 float b = 11; // b wird 11.0 u. v. m Die Einführung abstrakter Datentypen verlangt in gewissen Anwendungsfällen aber etwas mehr Aufwand. Dies gilt im Besonderen, wenn ein Objekt wieder in einen Standarddatentyp umgewandelt werden muß. Hierfür verwendet man in C++ Memberfunktionen, die ebenfalls durch das Schlüsselwort operator gekennzeichnet werden. Hinsichtlich der Syntax gibt es aber einige Einschränkungen: Syntax: operator <typ> ( ); typ: Datentyp in C++ ( int, char,..., Klassentyp, etc. ), kein Array Bemerkungen: 1. keine Parameter (nur mit dem Objekt selbst wird gearbeitet) 2. kein Rückgabetyp (der Rückgabetyp wird durch die Typbezeichnung der Operatorfunktion festgelegt) Auch für diese Anwendung sei zum Abschluß ein Beispiel angegeben: Beispiel: bsp_20.cpp

125

// bsp_20.cpp Beispiel zeigt Typumwandlung mit Klassenmethode #include <iostream.h> class test1 // klasse test1 { public: test1() { } // standardkonstruktor test1(float rez, float imz) // 2. konstruktor (zur init) { re=rez; im=imz; } operator int (void) // Syntax beachten { // explizite typumwandlung return re; } operator char (void) { return re; // umwandlung in char } private: float re,im; }; // ende der klasse void main(void) { test1 k1(7.5,3); // erste zahl int zahl=k1; // wäre sonst nicht möglich cout <<"\nzahl = "<<zahl; char zahl1 = k1; // wäre auch nicht möglich cout <<"\nzahl1 = "<<zahl1; // durch den wert 7.5 wird // beep } // erzeugt (char-konvertierung)

126

8.6.7 Überlagerung des Funktionsaufrufoperators () (Zusatzinformation)

Sofern dieser Operator für eine Klasse überladen wird, kann man Objekte der entsprechenden Klasse wie Funktionen behandeln. Beispiel: class test1 { private: float re, im; public: test1() {re=3;im=4;} float operator () () {return (sqrt(re*re+im*im));} // betrag test1 }; void main(void) { float betrag; test1 k1; betrag=k1(); // betrag }

8.6.8 Überlagerung des Operators ' ->' (Überblick, Zusatzinformation)

Die Funktion zur Überlagerung des Zeigeroperators darf - keine Argumente haben und muß als - Ergebnistyp einen Zeiger auf die eigene Klasse oder auf eine andere Klasse haben Beispiel: class test1 {... test1 * operator->(void); // prototyp };...

127

8.6.9 Die Überlagerung des <<-Operators (Zusatzinformation)

In Verbindung mit Objekten ist die Überlagerung des o. g. Operators von großer Bedeutung. Angenommen k1 sei ein Objekt der Klasse "test1". Die Anweisung cout << k1; wäre wünschenswert, funktioniert aber noch nicht. Der <<- Operator ist ein bereits überlagerter Operator. Zum einen dient er bereits im klassischen C zur Bitmanipulation. Andererseits überlädt die Klasse ostream diesen Operator, um Ausgaben über cout zu ermöglichen. (cout ist ein Objekt dieser Klasse). Um nun den <<-Operator auch für Objekte einsetzen zu können, könnte die Datei iostream.h verändert werden. Dies ist aber nicht empfehlenswert und widerspricht auch einer der grundlegenden Regeln der OOP. Günstiger ist die Anpassung der Klasse test1. Auszug aus "iostream.h" Frage: Um eine Anweisung cout <<k1 zu ermöglichen könnte es zwei Möglichkeiten geben. Welche wählt man?: a) Klassenmethode b) Freundfunktion zu a): dann geht nur k1 <<cout; Beispiel: aus bsp_21.cpp class test1 {... void operator <<(ostream & os1) // ostream: klasse für cout { os1<<"realteil: "<<re <<"imaginärteil: " << im; } } Der Compiler macht (wie gehabt) daraus: k1.operator <<(cout); Dies ist sehr unschön. Aber wir erinnern uns!! Sofern man Memberfunktionen zur Operatorüberlagerung verwendet, muß an erster Stelle das aufrufende Objekt stehen (ähnlich wie bei der Handhabung von Konstanten).

128

Auszug aus "iostream.h>

Prototypen zur Überlagerung des <<-Operators

/* Formated insertion operations */ ostream & _Cdecl operator<< ( signed char); ostream & _Cdecl operator<< (unsigned char); ostream & _Cdecl operator<< (short); ostream & _Cdecl operator<< (unsigned short); ostream & _Cdecl operator<< (int); ostream & _Cdecl operator<< (unsigned int); ostream & _Cdecl operator<< (long); ostream & _Cdecl operator<< (unsigned long); ostream & _Cdecl operator<< (float); ostream & _Cdecl operator<< (double); ostream & _Cdecl operator<< (long double);

129

// bsp_21.cpp Überlagerung des <<-operators #include <iostream.h> #include <math.h> class test1 { public: test1(float rez,float imz) {re=rez;im=imz;} // friend void operator <<(ostream &, test1 &); void operator <<(ostream &); test1 operator +(test1 k2) { float sre,sim; sre=k2.re+re; sim=k2.im+im; return test1(sre,sim); } private: float re,im; }; /* void operator <<(ostream &os1, test1 &k1) { os1 << "\nrealteil: " <<k1.re <<" imagin„rteil: " <<k1.im; return os1; } */ void test1:: operator <<(ostream &os1) { os1 << "\nrealteil: " <<re <<" imagin„rteil: " <<im; } void main(void) { test1 k0(3.3,4.5); test1 k1(6.6,7.7); k0<<cout; cout <<k1; cout <<k1 <<k1; cout <<k1+k0; cout <<"\n\nende\n"; }

130

Ausweg: Anwendung von Freundfunktionen, die ja das Objekt als Parameter benötigen. b) Freundfunktion Hier muß das Objekt also nicht an erster Stelle stehen. Nach entsprechender Umwandlung der Memberfunktion wäre ein Beispiel: Beispiel: immer noch bsp_21.cpp friend void operator <<(ostream &, const test1 &); void operator <<(ostream & os1, const test1 &k1) { os1 << "...... } Jetzt ist die Anweisung "cout <<k1;" korrekt Aber !!! Anweisungen, wie z. B. Beispiel: immer noch bsp_21.cpp cout <<k1 <<k1 oder cout <<"der wert des objektes ist" << k1; funktionieren nicht. Warum nicht?? Vereinbarungsgemäß (in iostream.h) muß links vom Operator << ein Objekt der Klasse ostream stehen. Dies ist bei cout <<k1; erfüllt, da cout ein Objekt der Klasse ostream ist. Für Anweisungen, wie cout << k1 <<k1 gilt dies nicht. C++ liest Anweisungen von rechts nach links. Das bedeutet, daß obige Anweisung wie folgt abgearbeitet wird: (cout <<k1)<<k1; Der Klammerausdruck steht wiederum links neben dem <<-Operator, d.h. auch der Klammerausdruck muß ein Objekt der Klasse ostream.h sein.

131

Was bedeutet dies? Auch unsere Freundfunktion zur Überlagerung des <<-Operators muß als Ergebnis ein ostream-Objekt zurückgeben. Beispiel: immer noch bsp_21.cpp ostream operator <<(ostream &os1, const test1 &k1) { os1<<"..... return os1; }; Dies ist immer noch nicht korrekt. Der Funktion wird eine Objektreferenz auf ostream übergeben. Damit mit dem gleichen Objekt weitergearbeitet werden kann und muß, muß auch der Rückgabewert vom Typ Referenz sein, also ostream & operator <<....

8.6.10 Standardanweisungen auf Objekte (Zusatzinformation)

Dies soll am Beispiel der for-Schleife demonstriert werden. Klassisch: for (i=100;i<=103;i++) { } jetzt auf ein Objekt der Klasse test1: for (k1=100;k1<=103;k1++) Welche Funktionen sind erforderlich ? 1. Init (k1=100); a) Konstruktor (mit einem Parameter) b) Überlagerung des =-zeichens Übergabe: float Rückgabe: test1 2. Abfrage (k1<=103); Überlagerung des <=-operators Übergabe: float Rückgabe: TRUE/FALSE 3. Wiederinit (k1++); Überlagerung des ++-Operators Übergabe : () oder (int=0) Rückgabe : z.B. void

132

Beispiel: bsp_22.cpp Überlagerung der for-schleife mit Objekten #include <iostream.h> class test1 { public: test1(float wert1) // konstruktor {re=im=wert1;} int operator <=(float wert1) { if (im<=wert1) return 1; // true else return 0; // false } void operator ++() {im=im+1;}

private:float re,im; }; void main(void) { cout <<"\n\nbeginn"; test1 k1(200); for(k1=100;k1<=103;k1++) cout <<"\nschleife"; cout<<"\n\nende"; } vereinfachtes Anwendungsbeispiel aus der Praxis: Aufgabenstellung: Entwerfen Sie eine Klasse zur Berechnung eines komplexen Widerstandes (Reihenschal- tung aus R und L. Eckpunkte: - als Test soll genügen: Eingabe für R und L - Frequenz von 0-10 MHz (Schrittweite 1 MHz) - Realisierung einer entsprechenden for-schleife, die für Objekte geeignet ist - Ausgabe der Teilergebnisse mit "cout << objekt" Lösungsmöglichkeit: bsp_23.cpp

133

// bsp_23.cpp // berechnung eines widerstandes (reihenschaltung von r und l ) #include <iostream.h> #include <math.h> class test1 { private: double re,im,sp; long unsigned frequenz; public: test1() { } test1(long unsigned f) // startfrequenz for-schleife { cout <<"\nwiderstand ";cin>>re; cout <<"\nspule ";cin>>sp; frequenz=f; im=2*3.14*(double)f*sp; } int operator <(long unsigned f) // für for-schleife { if(frequenz<f)return 1; else return 0; } void operator ++() {frequenz=frequenz+1E+6; im=2*3.14*frequenz*sp;} friend ostream & operator <<(ostream &, const test1 &); }; ostream & operator <<(ostream & os1, const test1 &k1) { os1 << "\nfrequenz:\t" <<k1.frequenz <<"\twiderstandsbetrag: " ; os1 << (sqrt(k1.re*k1.re+k1.im*k1.im)) << "\twinkel: "; os1 << atan (k1.im/k1.re); return os1; } void main (void) { long unsigned hmegaherz=1E+7; test1 k1; for(k1=0;k1<hmegaherz;k1++) cout <<k1; }

134

8.7 Der „this“ - Zeiger in C++ (Zusatzinformation)

Genau wie man Standarddatentypen miteinander verknüpfen (vergleichen, etc...) kann, muß diese Forderungen auch für selbstdefinierte Datentypen realisiert werden. Bisher gibt es aber keine Möglichkeiten, um z.B. Objekte miteinander zu vergleichen. Stillschweigend wird in C++ für jedes Objekt ein quasi unsichtbarer Zeiger mitgeführt, der vor allen Dingen das Arbeiten mit mehreren Objekten gestattet. Dieser Zeiger heißt in C++ „this“. „this“ steht dabei für die Adresse eines (besser des aktuellen oder aufrufenden) Objektes. Zur Veranschaulichung dieses Zeigers dient die nachfolgende Grafik. Zur Veranschaulichung des „ this“ -Zeigers Das folgende Programm zeigt die Handhabung des Zeigers „this“. Dabei werden die Realteile zweier Zahlen miteinander verglichen. bsp_24.cpp Allgemeine Handhabung des „ this“ -Zeigers Wichtig: Auf den „this“ - Zeiger kann immer nur in einer Klassenmethode zugegriffen werden. Warum ist das so?? Antwort: Vorher muß ja die Zuordnung zu einem Objekt erfolgt sein, auf das dann „this“ zeigen kann. Will man nun Objekte beispielsweise vergleichen, und das Ergebnis einem dritten Objekt zuweisen, käme man ohne Anwendung des „this“-Zeigers nur sehr umständlich zum Ziel.

Worin besteht nämlich die Aufgabe? Es ist schon sinnvoll an eine Vergleichsmethode das eine Objekt als Referenz zu übergeben. Will man, um auf eben erläutertes Beispiel zurückzukommen, zwei Objekte vergleichen und das größere zurückgeben, so macht es keine Schwierigkeiten, wenn dieses das explizite Objekt, d.h. das als Referenz übergebene Objekt, ist.

Zur Veranschaulichung des „this“ - Zeigers in

C ++

135

Die

erzeugt das Objekt

z.B.: k1.erg(); z.B.: k2.erg(); Kap2,16,Wenzel

z.B. irgendeine Klasse ( in unserem Beispiel :

„class test1“ )

Anweisung: test1 k1(1,3);

Anweisung: test1 k2 (3,4);

k1 k2

„this“ zeigt auf k1

Zugriff auf Elemente des Objektes erfolgen eigentlich über

this -> <element>; (z. B. this -> re;)

„this“ zeigt auf k2

136

// bsp_24.cpp // beispiel veranschaulicht die allgemeine wirkungsweise // des "this"-zeigers (worauf zeigt er eigentlich) #include <iostream.h> class test1 // klasse test1 { public: test1(float rez, float imz) // konstruktor (zur init) { this->re=rez; // this entspricht der jeweiliegen this->im=imz; // adresse des objektes } //anweisungen entsprechen: // re=rez; im=imz; float vergl(test1 &); // übergabe der zweiten zahl als // referenz (explizit); 1. zahl: implizit private: float re,im; }; //ende der klasse float test1::vergl(test1 & wert2) { if (wert2.re > this->re) // this->re = re vom aufrufenden obj. return wert2.re; // wert2.re = re vom übergeb. obj. else return this->re; // re vom aufrufenden objekt ist // größer } void main(void) { float erg; test1 k1(1,3); // erste zahl test1 k2(3,4); // zweite zahl erg=k1.vergl(k2); // vergl. gibt float zurück // this zeigt auf k1 (aufruf. obj) cout <<"\ngrößter realwert "<<erg; }

137

Beispiel: test1 test1 :: vergl(test1 &k22) { if (k22.re>re) return k22; } Was aber, wenn nun das erste, d. h. aufrufende oder implizite Objekt das größere von beiden ist?? Der this-Zeiger enthält die Adresse des impliziten Objektes. Wenn man sich die Zeigerarithmetik von C in’s Gedächtnis ruft, errinert man sich: <zeiger> : Adresse *<zeiger> : Inhalt der Adresse Das bedeutet aber, daß „ *this“ genau das Objekt selbst darstellt, da „this“ die Adresse des Objektes ist. bsp_25.cpp Vergleich von Objekten mittels „*this“

138

// bsp_25.cpp // beispiel zeigt anwendung des this-zeigers bei der bearbeitung // mehrerer objekte #include <iostream.h> class test1 // klasse test1 { public: test1() { re=7; } // dummy für debugger test1(float rez, float imz) // konstruktor (zur init) { re=rez; im=imz; } test1 & vergl(test1 &); // vergleich beider realteile // parameter referenz (wert2) // rückgabe des entsprechenden // objektes mit dem größeren // realteil an das ergebnisobj., // deshalb rückgabe als referenz void erg () { cout <<"\ngrößter realwert " <<re; } private: float re,im; // realteil, imaginärteil }; test1 & test1::vergl(test1 & wert2) { if(wert2.re >re) // re steht für k1, wert2.re für k2 return wert2; else return *this; // *this ist k1, damit wäre realteil } // von k1 größer als der von k2; void main(void) { test1 k1(1,3); test1 k2(3,4); test1 k3; k3=k1.vergl(k2); // this steht auf k1,bedingt durch werte // steht hier dann k3 = k2 k3.erg(); }

139

8.8 Zusammenfassung zum Kapitel „Klassen“

Klassen bilden das Kernstück der objektorientierten Programmierung in jeder OOP-Sprache, nicht nur in C++. Sie beinhalten die eigentlichen Wesensmerkmale der OOP und wurden deshalb so ausführlich behandelt. Diese Merkmale seien abschließend noch einmal in einer Übersicht aufgeführt: Zusammenfassung zur Thematik „Klassen“

140

Zusammenfassung zur Thematik „Klassen“

Klassen--> abstrakte Datentypen realisierbar

- Kapselung von Daten und Methoden

Objekte - Repräsentanten von Klassen

Information-Hidding - auf Daten wird in der Regel nur mit Methoden der Klasse zugegriffen (extreme Datensicherheit) - der Aufbau der Klassen ist für den Anwender uninteressant

Anwendung des Sendens und Empfangens von Botschaften

- Definition der Schnittstelle zur Außenwelt ist maßgebend, nicht die Struktur der Daten - daran muß man sich am meisten gewöhnen

Freundfunktionen und Freundklassen - Zugriff auch auf versteckte Daten - für den erlaubten Zugriff einer Funktion auf mehrere Objekte - Berücksichtigung der Referenzmethode, um mit Objekten arbeiten zu können

141

Konstruktoren und Destruktoren - erleichtern die Handhabung von Klassen - Überlagerung von Konstruktoren für universelle Klassennutzung (eine Form der OOP-typischen Polymorphie) - Spezialaufgaben (z. B. Aliasvermeidung, Frei- gabe und Reservierung von Speicherplätzen )

Operatorüberlagerung - ermöglichen die Realisierung aller denkbaren Programminhalte - weitere Form der Polymorphie - erleichtern auch hier erheblich den Programmier- aufwand (Programmierer muß nicht den Klassen- aufbau kennen) - Verbesserung der Lesbarkeit