objektorientierte softwareentwicklung mit c++ · objektorientierte softwareentwicklung mit c++ / ws...

105
Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-1 Objektorientierte Softwareentwicklung mit C++ Skript: Dr. Matthias Grabert 17.September 2001 Fakultät für Mathematik und Wirtschaftswissenschaften Abteilung Angewandte Informationsverarbeitung Vorlesungsbegleiter für das WS 2001/2002 Universität Ulm Anmerkungen: Das vorliegende Skript ist kein Lehrbuch über C++, sondern ist als vorlesungsbegleitende Skizze gedacht, also als Ergänzung und nicht Ersatz der Vorlesung. Die wesentlichen Sprachelemente der Programmiersprache C werden für das Verständnis der Vorlesung vorausgesetzt (z.B. Arrays, Zeiger, Parameterübergabe bei Funktionen, Kontrollstrukturen). Die Vorlesung wurde auf einem GNU-C++-Compiler unter Solaris entwickelt: thales$ g++ -v Reading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/2.95.3/specs gcc version 2.95.3 20010315 (release) Die Beispiele sollen jeweils gewisse Aspekte verdeutlichen und erheben nicht den Anspruch von Robustheit und Zuverlässigkeit. Man kann alles anders und besser machen. Im Laufe der Vorlesung wird neben den grundlegenden objektorientierten Konzepten in der Programmiersprache C++ auch auf die Standard Library und auf die Standard Template Library eingegangen. Proprietäre Bibliotheken (wie z.B. die Microsoft Foundation Classes) sind nicht Gegenstand der Vorlesung.

Upload: vuongnhan

Post on 05-May-2019

226 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-1

Objektor ientierte Softwareentwicklung mit C++

Skr ipt: Dr . Matthias Grabert 17.September 2001

Fakultät für M athematik und Wir tschaftswissenschaften

Abteilung Angewandte Informationsverarbeitung Vor lesungsbegleiter für das WS 2001/2002

Universität Ulm Anmerkungen:

• Das vorliegende Skript ist kein Lehrbuch über C++, sondern ist als vorlesungsbegleitende Skizze gedacht, also als Ergänzung und nicht Ersatz der Vorlesung.

• Die wesentlichen Sprachelemente der Programmiersprache C werden für das Verständnis der Vorlesung vorausgesetzt (z.B. Arrays, Zeiger, Parameterübergabe bei Funktionen, Kontrollstrukturen).

• Die Vorlesung wurde auf einem GNU-C++-Compiler unter Solaris entwickelt: thales$ g++ -v Reading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/2.95.3/specs gcc version 2.95.3 20010315 (release)

• Die Beispiele sollen jeweils gewisse Aspekte verdeutlichen und erheben nicht den Anspruch

von Robustheit und Zuverlässigkeit. Man kann alles anders und besser machen. • Im Laufe der Vorlesung wird neben den grundlegenden objektorientierten Konzepten in der

Programmiersprache C++ auch auf die Standard Library und auf die Standard Template Library eingegangen. Proprietäre Bibliotheken (wie z.B. die Microsoft Foundation Classes) sind nicht Gegenstand der Vorlesung.

Page 2: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-2

Literatur: (bitte jeweils die neuesten Ausgaben beachten!)

• [Aupperle97] Martin Aupperle: Die Kunst der objektorientieren Programmierung mit C++ . Vieweg, Braunschweig 1997

• [Booch94] Grady Booch, Objektorientierte Analyse und Design, 1.Auflage, Addison-Wesley, Bonn, 1994

• [Borchert01] Andreas Borchert, Computer Science for Transfers, Einführung in OOP unter C++ und UML, Rochester Institute of Technology, USA, Fall 2001/1

• [Deitel01] Deitel and Deitel, ̀ C++ How to Program', Prentice Hall , Third Edition, 2001

• [Josuttis94] Niloalai Josuttis, OOP in C++ : von der Klasse zur Klassenbibliothek, Bonn, Addison-Wesley 1994

• [Kernighan90] Brian Kernighan & Dennis Ritchie, Programmieren in C, Hanser Verlag München, 1990

• [Oesterreich98] Bernd Oesterreich, Objektorientierte Softwareentwicklung: Analyse und Design mit der UML, 4.Auflage, Oldenbourg 1998

• [Prinz98] Peter Prinz & Ulla Kirch-Prinz: Objektorientiert Programmieren mit ANSI C++ , Prentice Hall , München 1998

• [Schreiner94] Axel Tobias Schreiner: Objektorientierte Programmierung mit ANSI C, Hanser Verlag, München 1994

• [Stroustrup00] Bjarne Stroustrup: Die C++ Programmiersprache - Professionelle Programmierung. Addison Wesley 2000

Webseiten/Compiler:

• ANSI-Standard ISO/IEC 14882 von 1998 (~1000 Seiten): http://www.ncits.org/cplusplus.htm

• "C++ Erfinder" Bjarne Stroustrup: http://www.research.att.com/~bs/C++.html • Learning C++ as a New Language: http://www.research.att.com/~bs/new_learning.pdf • Freie Compiler: http://www.research.att.com/~bs/compilers.html • Der Borland C++ 5.5 (Windows) ist z.B. frei seit Anfang 2000

http://community.borland.com • Gnu C++ Compiler für (fast alle) Platformen: http://www.gnu.org/software/gcc/gcc.html • QT-GUI-Library: http://www.trolltech.com • UML: www.jeckle.de/uml und http://ivs.cs.uni-magdeburg.de/~dumke/UML

Page 3: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-3

Inhaltsverzeichnis 1 Historische Entwicklung der Programmiersprachen................................................................1-5 2 Motivation - Warum wird die prozedurale Programmierung abgelöst?...................................2-6 3 Von C nach C++.......................................................................................................................3-8

3.1 Einordnung.......................................................................................................................3-8 3.2 Übersetzung von C-Programmen auf C++-Compilern ....................................................3-8

3.2.1 Neue Schlüsselwörter in der Sprache.......................................................................3-9 3.2.2 gcc-Compileraufruf auf den SUNs...........................................................................3-9 3.2.3 Strengere Typprüfung/Prototyping...........................................................................3-9 3.2.4 Deklaration von C-Modulen in C++-Programmen ................................................3-10 3.2.5 Feinheiten bei der Vektorinitialisierung................................................................. 3-11

3.3 Nicht-objektorientierte Erweiterungen in C++ ..............................................................3-11 3.3.1 Neuer Kommentarstil .............................................................................................3-11 3.3.2 Mischen von Deklarationen und Statements..........................................................3-11 3.3.3 Defaultargumente bei Funktionen..........................................................................3-12 3.3.4 Referenzen auf Variablen - Call by Reference in Funktionsaufrufen....................3-13 3.3.5 Überladen von Funktionen.....................................................................................3-14 3.3.6 Funktionstemplates................................................................................................. 3-16 3.3.7 Inlinefunktionen .....................................................................................................3-18 3.3.8 Neue bzw. anders verwendete Datentypen.............................................................3-21 3.3.9 Kleine Einführung in das Exceptionhandling ........................................................3-23 3.3.10 Speichermanagement via new und delete ..............................................................3-25 3.3.11 Namensräume.........................................................................................................3-27 3.3.12 Ein Beispiel zu Namensräumen und Modularisierung...........................................3-29

4 Datenabstraktionen.................................................................................................................4-32 4.1 Klassen ...........................................................................................................................4-32

4.1.1 Elementfunktionen ................................................................................................. 4-32 4.1.2 Klassendefinitionen und Zugriffskontrolle ............................................................4-34 4.1.3 Der Konstruktor......................................................................................................4-35 4.1.4 Der Destruktor........................................................................................................4-36 4.1.5 Verweis auf das aktuelle Objekt in einer Elementfunktion: this............................4-37 4.1.6 Der Kopierkonstruktor und der Operator =............................................................4-37 4.1.7 Statische Klassenelemente.....................................................................................4-38 4.1.8 Definieren bzw. Überladen von Operatoren..........................................................4-39 4.1.9 Ein komplettes Beispielprogramm für ganzzahlige Brüche...................................4-40 4.1.10 Ein Freund fürs Leben: friend.............................................................................4-44 4.1.11 Liste überladbarer Operatoren................................................................................4-44 4.1.12 Operatoren für unterschiedliche Datentypen..........................................................4-45

4.2 Initialisierung von Objekten über den ":" hinter der Konstruktorfunktion ....................4-45 4.3 Ein Template für sichere dynamische Vektoren ............................................................4-45 4.4 Begriffsklärung und Definitionen ..................................................................................4-48

5 Nützliche Hil fsmittel in C++..................................................................................................5-49 5.1 Strings.............................................................................................................................5-49 5.2 Vektoren.........................................................................................................................5-56 5.3 IO-Streams......................................................................................................................5-57

5.3.1 Ein einleitendes Beispielprogramm........................................................................5-59 5.3.2 Standardstreams und -operatoren...........................................................................5-60 5.3.3 Fehlerzustände........................................................................................................5-60 5.3.4 Standardelementfunktionen zur Eingabe................................................................5-61 5.3.5 Standardelementfunktionen zur Ausgabe...............................................................5-61 5.3.6 Verknüpfung von Ein- und Ausgabe......................................................................5-61

Page 4: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-4

5.3.7 Kleines Kopierprogramm mit Statusabfrage..........................................................5-62 5.3.8 Manipulatoren ........................................................................................................5-62 5.3.9 Formatdefinitionen................................................................................................. 5-62 5.3.10 Dateizugriff ............................................................................................................5-63

5.4 Die String-Stream-Klasse - Nearly everything is a stream. ...........................................5-65 6 Vererbung...............................................................................................................................6-67

6.1 Objekte als Klassenelemente..........................................................................................6-67 6.2 Vererbung.......................................................................................................................6-67 6.3 Arten der Vererbung.......................................................................................................6-70 6.4 Implizite Typumwandlungen und Zuweisungen............................................................6-70 6.5 Polymorphie...................................................................................................................6-71

6.5.1 Das Schlüsselwort virtual .......................................................................................6-73 6.5.2 Der Begriff "Polymorphismus" ..............................................................................6-74 6.5.3 Eigenschaften virtueller Funktionen ......................................................................6-74 6.5.4 Virtuelle Destruktoren............................................................................................6-74 6.5.5 Abstrakte Klassen...................................................................................................6-75

6.6 Dynamische Casts und Objekt-Ids.................................................................................6-77 6.7 Mehrfachvererbung........................................................................................................6-79 6.8 Virtuelle Basisklassen ....................................................................................................6-80

7 Einblick in die Standard Template Library (STL) von C++ ..................................................7-82 7.1 Containertemplates.........................................................................................................7-82 7.2 Iteratoren: Zugriff auf Elemente beliebiger Container...................................................7-83 7.3 Operationen und Algorithmen auf den Elementen der Container..................................7-84

7.3.1 Einige einführende Beispielprogramme................................................................. 7-85 7.3.2 Übersicht: Nicht-modifizierende Sequenzoperationen ..........................................7-88 7.3.3 Übersicht: Modifizierende Sequenzoperationen ....................................................7-89 7.3.4 Übersicht: Sequenzen sortieren..............................................................................7-89 7.3.5 Übersicht: Mengenalgorithmen..............................................................................7-90 7.3.6 Übersicht: Minimum und Maximum......................................................................7-90 7.3.7 Übersicht: Numerische Algorithmen......................................................................7-90 7.3.8 Funktionsobjekte (Funktoren) ................................................................................7-91

8 Übersicht über die UML.........................................................................................................8-93 8.1 Klassendiagramme.........................................................................................................8-94

8.1.1 Basisdokumentation und Vererbung......................................................................8-94 8.1.2 Assoziationen .........................................................................................................8-96

9 Anhang ...................................................................................................................................9-98 9.1 Bubblesort als Funktionstemplate..................................................................................9-98 9.2 Lebenszyklus von Objekten .........................................................................................9-100 9.3 Achtung bei Operatorenfunktionen mit einer Referenz auf temporäre Objekte..........9-102 9.4 Elementinitialisierung beim Konstruktor via ":"-Operator ..........................................9-104

Page 5: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5

1 Historische Entwicklung der Programmiersprachen

• I started work on what became C++ in 1979. The initial version was called "C with

Classes". The first version of C++ was used internally in AT&T in August 1983. The name "C++ " was used late that year. The first commercial implementation was released October 1985 at the same time as the publication of the 1st edition of "The C++ Programming Language". Templates and exception handling were included later in the 1980's and documented in "The Annotated C++ Reference Manual" and "The C++ Programming Language (2rd Edition)". The current definition of C++ is "The ISO C++ Standard" described in "The C++ Programming Language (3rd Edition)." [Bjarne Stroustrup in http://www.research.att.com/~bs/C++.html]

• Aktueller Stand: ISO Standard 14882 von 1998 (Gegenstand der Vorlesung!)

Maschinensprache/Assembler

CobolFortranLispAlgol 60

PL/1BasicAlgol 68Simula 67

PrologPascalC

Modula-2AdaSmalltalk-80

OberonC++Eiffel

C#

PerlJava Visual Basic

1950

1960

1970

1980

1990

1995

2000

Page 6: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 2-6

2 Motivation - Warum wird die prozedurale Programmierung abgelöst? (s.a. [Borchert01], [Prinz98]) 1. Die typischen klassischen prozeduralen Programmiersprachen (C, Fortran, Pascal ...) tragen meist die folgende Struktur in sich:

• Es gibt eine Reihe separat compili erbarer Module (und vorcompili erter Bibliotheken), die in einem zweiten Schritt zu einem ausführbaren Programm zusammengelinkt werden können.

• Jedes einzelne Teilmodul besteht i.W. aus (für das Modul) global vorhandenen Funktionen und Variablen.

• Parameter und globale Variablen (incl. dynamischer Daten) werden zur unbeschränkten Kommunikation zwischen den einzelnen Programmteilen (Module, Funktionen) verwenden.

Auftretende Probleme:

• Die Programme werden meist um vorhandene Variablen und Datenstrukturen herum gebaut. Alle Programmteile greifen auf die globalen Variablen zu. (Um in C eine Variable vor dem Zugriff anderer Module zu schützen, muss sie explizit als static deklariert werden).

• Das wiederum macht z.B. die Fehlersuche sehr diff izil , da i.W. jedes Programmteil als "Täter" in Frage kommt.

2. Die zweite Generation der prozeduralen Programmiersprachen unterstützt wesentlich stärker das Modulkonzept (Modula-2, Ada):

• Innere Variablen der Module sind stärker geschützt und es wird transparent gemacht, wer welche Variablen von welchem Modul benützt (Definitionmodule / Implementationmodul)

• Das gleiche gilt für Prozeduren. Damit Variablen und Prozeduren von anderen Modulen benützt werden können, müssen sie explizit im Definitionmodul genannt werden.

• Abstrakte Datentypen versuchen die Implementierungsdetails von den Schnittstellen zu entkoppeln und damit zu verbergen (z.B. lineare Liste, Stack, Sortierverfahren)

Kommunikation

über Daten

Daten Daten

Funktion Funktion Funktion Kommunikation

über Daten

Page 7: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 2-7

Probleme:

• Die Schnittstellen reflektieren immer noch stark die zugrunde liegende Implementierung (1:1-Beziehung).

• Eine Zusammenfassung/Gruppierung mehrerer Implementierungen (z.B. Sortierverfahren), die ähnliche Schnittstellen haben, ist (aufgrund der 1:1-Beziehung) schwer möglich.

3. In OO-Sprachen werden alle Daten in Form von Objekten repräsentiert (meistens mit Ausnahme der Basistypen int, char, float etc.).

• Objekte werden (zumindest implizit) durch Pointer repräsentiert. • Objekte setzen sich aus anderen Objekten (Container) oder aus (aggregierten)

Basistypen zusammen. • Objekte geben sich "gekapselt" und geschützt, d.h. Zugriff von außen erfolgt nur über

definierte Methoden (Prozeduren) oder auf nach außen explizit freigegebene Daten. • Alle Objekte werden aus sog. Klassen erzeugt bzw. "instanziiert". Die Klassen

definieren die Methoden für den Zugriff auf Ihre Objekte und legen fest, welche Daten von außen verändert und eingesehen werden können.

• Man kann neue Klassen leicht aus bereits bestehenden anderen Klassen bilden. Diese neuen Klassen erben dann die Eigenschaften der Basisklassen.

• Beim Erzeugen der Objekte werden sog. Konstruktoren aufgerufen, beim Löschen der Objekte sog. Destruktoren. Beides sind Funktionen, die in den Klassen definiert werden müssen.

• In vielen Fällen wird die reale Welt besser durch Objekte und Beziehungen zwischen Objekten repräsentiert als nur durch Prozeduren und Daten (Bsp.: Sportverein: Mitglieder, Abteilungen, Funktionen der Mitglieder, Mitgliedschaften in verschiedenen Abteilungen etc.)

• Man kann viele Eigenschaften der OO-Programmierung durch prozedurale Programmierung nachbauen bzw. simulieren. Sie werden in den Programmiersprachen aber nicht genuin unterstützt (siehe z.B. [Schreiner94])

OO-Sprachen unterstützen wesentlich besser als Ihre Vorgänger die folgenden Programmier-paradigmen:

• (Daten-)Abstraktionen und Kapselung (Objekte statt "flache" Datenstrukturen) • Wiederverwendbarkeit (Prinzip der Vererbung) • Informationhiding

Objekt 2

Eigenschaften

Methoden

Objekt 1

Eigenschaften

Methoden

Kommunikation

über Methoden

Page 8: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-8

3 Von C nach C++

3.1 Einordnung [Prinz98] C++ ist keine "reine" objektorientierte Sprache, wie z.B. Smalltalk oder Eiffel. Sie ist historisch aus der Programmiersprache C entstanden bzw. hat diese um objektorientierte Konzepte erweitert. Alte C-Programme laufen bis auf marginale Einschränkungen (s.u.) auch unter jedem C++-Compiler.

3.2 Übersetzung von C-Programmen auf C++-Compilern Prinzipiell i st C++ so konzipiert, dass sämtliche C-Programme auch auf C++-Compilern ablaufen. Probleme und Fallstricke werden im folgenden erläutert.

C++ Standard 1998 ISO OOP ua. Erweiterungen: Inlinefunktionen, Referenzen, Operatorüberladung, Templates, ... OOP-Unterstützung: Klassen, Vererbung, Polymorphie, Objekttemplates, ...

C Standard von 1999 - ISO 9899 neuer Kommentarstil // , Mischung von Anweisung und Deklarationen ...

ANSI C Prototypen, erweiterte Standardbibliothek, Internationalisierung

C Kernighan-Ritchie-Standard

Page 9: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-9

3.2.1 Neue Schlüsselwörter in der Sprache Die folgenden Schlüsselwörter sind in C++ neu und dürfen daher nicht als Variablennamen oder Funktionsnamen verwendet werden [Stroustrup00]:

neue Schlüsselworte der Sprache C++ gegenüber C and catch explicit namespace or_eq template typename

and_eq class export new private this using

asm compl false not protected throw virtual

bitand const_cast friend not_eq public true wchar_t

bitor delete inline operator reinterpret_cast try xor

bool dynamic_cast mutuable or static_cast typeid xor_eq

3.2.2 gcc-Compileraufruf auf den SUNs Im folgenden werden C-Programme die Endung .c behalten und C++-Programme werden mit der Endung .cc gekennzeichnet. Es ist auch die Endung .cpp für C++-Programme möglich. Für die Vorlesung gil t der GNU-C bzw C++ Compiler als Referenz. Er unterscheidet C von C++ -Programmen an der Endung des Dateinamens. Will man explizit den C++-Compiler aufrufen, so heißt das Kommando g++ anstatt gcc. Auf der Thales muss zur Zeit noch die Umgebungsvariable LD_LIBRARY_PATH auf /usr/local/lib gesetzt werden, damit die C++-Standardlibrary beim Linken gefunden wird (z.B. in der Datei $HOME/.profile oder $HOME/.bashrc ) export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

3.2.3 Strengere Typprüfung/Prototyping Der C++-Compiler ist wesentlich strenger bei der Typprüfung: externe (Bücherei-)Funktionen müssen durch Funktionsprototypen (oder via #include ) deklariert werden, bevor sie benutzt werden können. Das folgende Programm läuft anstandslos durch einen C-Compiler, aber nicht durch einen C++-Compiler: thales$ cat fuenf.cc int main() { puts("fuenf = gerade"); } thales$ gcc fuenf.cc fuenf.cc: In function `int main()': fuenf.cc:3: implicit declaration of function `int puts(...)' Hier muss unbedingt die Funktion puts deklariert werden, was z.B. durch das Einfügen der Zeile #include <stdio.h> geschieht.

Page 10: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-10

Aufgrund der Abwärtskompatibilit ät von ANSI-C zu "Kernighan-Ritchie-C" bedeuten leere Klammern bei Funktionsdeklarationen, dass keine Informationen über die Parameter vorliegen. Bei C++ hingegen zeigen leere Klammern an, dass keine Parameter an die Funktion übergeben werden: int jod(); /* in C: Funktion gibt int zurück; beliebige Parameter */ int jod(); /* in C++:Funktion gibt Int zurück und hat keine Parameter * / Um in (ANSI-)C anzuzeigen, dass wirklich keine Parameter an die Funktion übergeben werden, mußte das Schlüsselwort void in die Parameterliste aufgenommen werden: int jod(void);

3.2.4 Deklaration von C-Modulen in C++-Programmen Um gezielt (eigene) C-Funktionen in einem C++-Programm verwenden zu können, müssen sie mit dem Schlüsselwort extern "C" gekennzeichnet werden: extern "C" int puts(char *s); /* Deklaration einer C - Funktion */ int main() { puts("jetzt klappt es auch ohne stdio.h"); } Sollen mehrere C-Funktionen verwendet werden, können sie auch in einem Block zusammengefaßt werden: extern "C" { int puts(const char *s); char *strchr(const char *s, const char ch); } Wenn man sich die Headerdateien der Standardbibliothek ansieht, erkennt man, dass dort bereits die Funktionen entsprechend durch bedingte Übersetzung deklariert werden: #ifdef __cplusplus extern "C" { #endif … extern FILE *fopen(const char *, const char *); … #ifdef __cplusplus } #endif Der Makro __cplusplus wird vom Compiler automatisch definiert, wenn es sich um ein C++-Programm handelt.

Page 11: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-11

3.2.5 Feinheiten bei der Vektorinitialisierung Bei manchen alten C-Compilern erzeugt die folgende Zeile char s[3] = "Hallo"; einen Vektor der Länge 6 (5 Zeichen + 0Byte) --- trotz der expliziten Angabe der Zahl 3 bei der Initialisierung. C++ nimmt den Programmierer beim Wort und erstellt einen Vektor der Länge 3 ("Hal") bzw. bricht die Initialisierung mit einer Fehlermeldung ab.

3.3 Nicht-objektorientierte Erweiterungen in C++

3.3.1 Neuer Kommentarstil In ANSI-C beginnt Kommentartext mit /* und geht ggf. über mehrere Zeilen bis zum */ In C++ (und auch im Standard C von 1999) gibt es eine weitere Kommentarmöglichkeit: angefangen ab // bis zum Ende der aktuellen Zeile (bis zum Newline). main() { int i = 3; /* * das ist ein alter C - Kommentar über mehrere Zeilen */ i++; // Dieser C++ - Kommentar endet in der aktuellen Zeile return i; }

3.3.2 Mischen von Deklarationen und Statements In ANSI-C gab es nach dem Funktionsheader einen Deklarationsblock, der durch die eigentlichen Programmstatements abgeschlossen war. Eine Deklaration nach einem Statement führte zu einem Syntaxfehler: #include <stdio.h> int main() { int i; char buf[1024]; // Variablendeklarationenen gets(buf); // Deklarationsende durch Sta tementbeginn

i = atoi(buf); float pi = 3.1415926; // Fehler in ANSI - C --- erlaubt in C++ und C99 printf("%d mal %f = %f \ n", i, pi, i*pi); return 0;

}

Page 12: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-12

In C++ können Deklarationen auch nach Statements kommen. Die folgende for-Schleife ist z.B. bei C++ Compilern möglich (nicht jedoch mit alten ANSI-C-Compilern!): thales$ cat for.cc #include <stdio.h> int main() { for (int i=0; i<3; i++) printf("i=%d\n", i);

// Achtung: i ist nach der for-Schleife undefiniert!! } thales$ gcc for.cc thales$ a.out i=0 i=1 i=2

3.3.3 Defaultargumente bei Funktionen In C++ ist es möglich, dass man für Funktionsparameter Standardvorgaben macht. Diese Parameter müssen beim Aufruf der Funktion nicht explizit angegeben werden. Parameter mit Defaultwerten müssen ans Ende der Parameterliste platziert werden: thales$ cat stand1.cc #include <stdio.h> void incr(int *x, int step=1) { *x += step; } int main() { int var = 5; incr(&var, 2); // Spezifikation der Schrittweite printf("var = %d\n", var); incr(&var); // nichts angegeben - Schrittweite = 1 printf("var = %d\n", var); } thales$ a.out var = 7 var = 8 thales$

Page 13: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-13

3.3.4 Referenzen auf Variablen - Call by Reference in Funktionsaufrufen Neu in C++ ist das Konzept von Referenzen auf bereits bestehende Variablen. Referenz und Original können nach der Zuweisung nicht mehr unterschieden werden. Dieses Konzept ergänzt die aus C bekannten Zeiger auf Objekte. thales$ cat ref1.cc #include <stdio.h> main() { int i = 314; // Das "Original" printf("i = %d\n", i); int &x = i; // x ist eine Referenz auf i printf("x = %d\n", x); x++; printf("i = %d, x = %d\n", i, x); } thales$ a.out i = 314 x = 314 i = 315, x = 315 Mit Referenzen (manchmal auch "Aliasnamen" genannt) kann man i.A. leichter umgehen als mit Zeigern. Es gibt im Wesentlichen zwei Unterschiede zwischen einer Referenz int &x = y; und einem Pointer int *x = &y; auf eine Variable:

• Der Pointer kann zur Laufzeit des Programms noch verändert werden (x = &z oder x++) , die Referenz nicht!

• Beim Pointer wird aufgrund des Zugriffs *x = 5 sofort ersichtlich, dass es sich um einen Verweis auf die eigentliche Speicherzelle handelt und nicht um das "Original".

Referenzen gibt es natürlich auch bei Funktionsaufrufen, und sie ermöglichen damit die in C bisher nicht vorhandene Parameterübergabe "Call by reference": thales$ cat stand2.cc #include <stdio.h> void incr(int &x, int step=1) { x += step; // parameter called by reference } int main() { int var = 5; incr(var, 2); // "&" kann jetzt weggelassen werden! printf("var = %d\n", var); } thales$ a.out var = 7

Page 14: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-14

Ein kleiner Nachteil bei dieser Art der Implementierung ist, dass beim Aufruf der Funktion incr() nicht sofort ersichtlich ist, dass die Funktion die übergebene Variable verändert. Dies muss der Beschreibung der Funktion bzw. der Funktionsdeklaration entnommen werden. Funktionen können auch Referenzen auf Variablen zurückliefern. Da muss man dann genau hinschauen, da das Resultat wiederum als Variable behandelt werden kann: thales$ cat refres.cc #include <stdio.h> int &refres() // Fkt. liefert Referenz auf Variable calls { static int calls = 0; // cal ls MUSS static sein calls ++; return calls; } main() { int &x = refres(); // x = Referenz auf Variable calls in refres() printf("x=%d \ n", x); ++refres(); // Refres wird aufgerufen UND calls um 1 erhöht printf("x=%d \ n", x); refres()=5; // calls (!!!) wird der Wert 5 zugewiesen printf("x=%d \ n", x); } thales$ a.out x=1 x=3 x=5

3.3.5 Überladen von Funktionen Funktionsnamen sollten "für sich selbst sprechen". Manchmal muss man aber die gleiche Funktion für verschiedene Datentypen schreiben --- und allein deswegen in C den Namen der Funktion variieren. Unter C++ werden Funktionen nicht über einen eindeutigen Funktionsnamen erkannt, sondern durch die Kombination Funktionsname & Datentypen der übergebenen Variablen. Man spricht in diesem Zusammenhang vom Überladen (overloading) von Funktionsnamen.

Page 15: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-15

thales$ cat overload1.cc #include <stdio.h> // Liefert Maximum des Doublearrays zurück double max(double x[], int len) { double max = x[0]; for (int i=1; i<len; i++) if (x[i]>max) max = x[i]; return max; } // Liefert Maximum des Integerarrays zurück int max(int x[], int len) { int max = x[0]; for (int i=1; i<len; i++) if (x[i]>max) max = x[i]; return max; } int main() { int intis[] = { 3,2,1, - 1}; double dbls[] = { 1.25, 1.37, 1.99, 3.1415926}; printf("Maximum der Integer ist %d \ n", max(intis, sizeof(intis)/sizeof(int))); printf("Ma ximum der Doubles ist %f \ n", max(dbls, sizeof(dbls)/sizeof(double))); } thales$ a.out Maximum der Integer ist 3 Maximum der Doubles ist 3.141593

Der Returntyp und die Namen der übergebenen Parameter einer Funktion gehören nicht zum eindeutigen Erkennungsmerkmal einer Funktion. So kann der Compiler z.B. die folgenden beiden Funktionen nicht unterscheiden: int find(char *key); char *find(char *str); Nicht auflösbare Mehrdeutigkeiten beim Overloading kann sich auch z.B. aufgrund von Default-argumenten ergeben. Das folgende Programm ist nicht compili erbar, obwohl sich die beiden Funktionen (signifikant) durch die Anzahl der Parameter unterscheiden:

Page 16: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-16

int mult(int a, int x, int plus=5) { return a * x + plus; } int mult(int a, int x) { return a * x; } int main() { int a = 12; int b = 10; int res = mult(a, b); }

3.3.6 Funktionstemplates Die Aufgabenstellung bezüglich der Maximumberechnung aus 3.3.5 kann man in C++ auch durch sog. Templates (Schablonen) lösen. Wie man bei dem Programm overload1.cc leicht sieht, unterscheiden sich die beiden Implementierungen der Funktion max() lediglich durch die Schlüsselwörter double und int --- alles andere ist identisch. Das führt zwangsläufig zur Idee von Funktionsschablonen, bei denen der Datentyp nur mehr durch einen Platzhalter gekennzeichnet ist: thales$ cat overload2.cc #include <stdio.h> // Liefert Maximum eines bel. Arrays zurück // Implementierung mit Funktionstemplates template<class T> T max(T x[], int l en) { T max = x[0]; for (int i=1; i<len; i++) if (x[i]>max) max = x[i]; return max; } int main() { int intis[] = { 3,2,1, - 1}; double dbls[] = { 1.25, 1.37, 1.99, 3.1415926}; printf("Maximum der Integer ist %d \ n", max(intis, sizeof(intis)/sizeof(int))); printf("Maximum der Doubles ist %f \ n", max(dbls, sizeof(dbls)/sizeof(double))); } thales$ a.out Maxi mum der Integer ist 3 Maximum der Doubles ist 3.141593 Das Schlüsselwort template leitet die Schablone ein. In spitzen Klammern <> steht das Schlüssel-wort class gefolgt von einem beliebigen Identifier (z.B. Buchstabenfolge) als Platzhalter für den zu ersetzenden Datentyp. Das Schlüsselwort class weist bereits darauf hin, dass hier nicht nur einfache Datentypen wie int, double etc. sondern auch Klassen bzw. Strukturen ersetzt werden

Page 17: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-17

können. In den spitzen Klammern können auch Platzhalter für mehrere Datentypen (bzw. Objekte) bereitgestellt werden: template<class X, class Y> void incr(X &x, Y y) { x += y; // addiere y zu x hinzu ... soweit es Sinn macht! } Der Compiler erzeugt beim Compili ervorgang jeweils eine Instanz (in Maschinencode) der Templatefunktion für jeden Aufruf mit einem bestimmten Datentyp. Beim Compili eren werden auch die Datentypen überprüft. Zum Beispiel könnte die obige Implementierung der Funktion incr nicht mit Variablen des Typs int und char* aufgerufen werden, da man keinen Char-Pointer (ohne Cast) zu einer Integer addieren kann. Templates können sich auch gegenseitig aufrufen: thales$ cat maxx.cc #include <stdio.h> template<class Platzhalter1> void swap(Platzhalter1 &x, Platzhalter1 &y) // vertausche x und y { Platzhalter1 hlp = x; x = y; y = hlp; } template<class F> void StoreMaxToX(F &x, F &y) // speichere Max in x, Min in y { if (x <y) swap(x, y); } int main() { int x = 10; int y = 22; StoreMaxToX(x, y); printf("x enthaelt jetzt das Maximum von x=%d und y=%d\n", x, y); } thales$ a.out x enthaelt jetzt das Maximum von x=22 und y=10

Page 18: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-18

Zusammenfassend: Templates sind parametrisierbare Schablonen für verwandter Funktionen, die sich nur durch Datentypen unterscheiden. Zusätzlich zu den eigentlichen Parametern der Funktion existieren Platzhalter für verschiedene Datentypen. Vorteile sind:

• Ein Template muss nur einmal codiert und damit auch nur einmal (algorithmisch) getestet werden.

• Dadurch entfallen Fehler bei der Mehrfachcodierung bzw. bei der Wartung des Codes. • Templates sind sehr mächtige Werkzeuge zur Erweiterung des Sprachumfangs. Es gibt für

C++ eine große Bücherei, die sog. Standard Template Library (STL), die später im bei den Klassen noch näher betrachtet wird.

• Man verwendet Templates auch im Zusammenhang von sog. Generic Programming ("programming with concepts") (s.a. http://www.cs.rpi.edu/~musser/gp/).

Ein ausführliches Beispiel für ein Funktionstemplate findet man im Anhang.

3.3.7 Inlinefunktionen Makros können in C mit Parametern versehen werden. Sie werden nicht nur für Konstanten verwendet, sondern oft auch für kleinere Funktionen, was einen Geschwindigkeitsvorteil gegenüber "echten" Funktionen bringt. Der grosse Nachteil i st aber, dass Seiteneffekte beim Aufruf des Makros oft übersehen werden. Gegeben sei das folgende Programm: thales$ cat makro.cc #include <stdio.h> #include <stdlib.h> #define max(x,y) (x>y?x:y) // Berechne Maximum zweier Zahlen int main(int argc, char **argv) { int a = (argc>1?atoi(argv[1]):0); // a = 1.Argument oder 0 int b = (argc>2?atoi(argv[2]):0); // b = 2.Argument oder 0 int max; max = max(++a, b); // Berechne Maximum von a+1 und b printf("max(%d, %d) = %d\n", a, b, max); } thales$ a.out 1 1 max(3, 1) = 3 thales$ a.out 1 2 max(2, 2) = 2 thales$ gcc -E makro.cc|tail -10 int main(int argc, char **argv) { int a = (argc>1?atoi(argv[1]):0); int b = (argc>2?atoi(argv[2]):0); int max; max = ( ++a > b ? ++a : b ) ; printf("max(%d, %d) = %d\n", a, b, max); }

Page 19: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-19

Wenn man sich mit gcc -E anschaut, was der C-Preprozessor aus dem Quelltext macht, sieht man die Ursache des Problems: beim Ersetzen des Makros max() wird durch den Fragezeichen-Doppel-punktoperator das 1.Argument des Makros (hier: ++a) zweimal bewertet, falls ++a größer als b ist, ansonsten nur einmal. Diese Schwachstelle von parametrisierbaren Makros und die Tatsache, dass sie nur schlecht mehrzeili ge Berechnungen enthalten können, hat in C++ zum Konzept der Inline-Funktionen geführt. thales$ cat inline.cc #include <stdio.h> #include <stdlib.h> inline int max(int x, int y) // "inline" erzeugt "Inlinefunktionen" { return (x>y?x:y); } int main(int argc, char **argv) { int a = (argc>1?atoi(argv[1]):0); int b = (argc>2?atoi(argv[2]):0); int maxim; maxim = max(++a, b); printf("max(%d, %d) = %d\n", a, b, maxim); } thales$ a.out 1 1 max(2, 1) = 2 thales$ a.out 1 2 max(2,2) = 2 thales$ gcc -E inline.cc|tail -20 # 2 "inline.cc" 2 inline int max(int x, int y) { return (x>y?x:y); } int main(int argc, char **argv) { int a = (argc>1?atoi(argv[1]):0); int b = (argc>2?atoi(argv[2]):0); int maxim; maxim = max(++a, b); printf("max(%d, %d) = %d\n", a, b, maxim); } Wie man sieht werden die Inline-Funktionen nicht durch den Preprozessor realisiert. Der Compiler setzt den Text der Funktion an die Stelle des Aufrufs im Hauptprogramm. Dadurch wird bei der Erzeugung des Maschinencodes die Ablegung der Rücksprungadresse auf dem Stack gespart. Dies macht sich insbesondere bemerkbar, wenn eine Funktion sehr häufig aufgerufen wird.

Page 20: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-20

thales$ cat perform.cc #include <stdio.h> #include <stdlib.h> long maximum(long a, long b) // Implementierung mit normaler Funktion { return (a>b?a:b); } int main(int argc, char **argv) { long limit = (argc>1?atol(argv[1]):0); long max = 0; for (long i=1; i<=limit; i++) max = maximum(i, max); printf("max = %ld\n", max); } thales$ time a.out 50000000 max = 50000000 real 0m8.534s user 0m8.300s sys 0m0.010s Implementiert man die Funktion maximum Inline inline long maximum(long a, long b) // Inline-Implementierung so ergibt sich das folgende Ergebnis (auf der Thales): thales$ time a.out 50000000 max = 50000000 real 0m6.352s user 0m6.300s sys 0m0.020s Es ist zu beachten, dass Inlinefunktionen nicht in anderen Modulen aufgerufen werden können. Die Implementierung der Funktion muss immer in dem selben Modul auch (textuell ) zur Verfügung stehen --- genau wie bei Makros. Deshalb müssen Inlinefunktionen, die in mehreren Module verwendet werden sollen, in einer Headerdatei abgelegt werden. Wird die Implementierung der Inlinefunktion zu umfangreich, kann es sein, dass der Compiler sie wie eine normale Funktion (also durch einen echten "Funktionsaufruf") behandelt und damit der Geschwindigkeitsgewinn verloren geht. Es wird dann beim Compili eren eine entsprechende Warnung ausgegeben.

Page 21: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-21

3.3.8 Neue bzw. anders verwendete Datentypen

3.3.8.1 Der (neue) Datentyp Boolean In C++ gibt es nun einen "echten" Datentyp Boolean, der die Werte true und false annehmen kann: thales$ cat bool.cc #include <stdio.h> int main() { bool flag = false; // generischer Datentype Boolean if (flag==true) // expliziter Vergleich puts("true"); if (!flag) // implizite Konvertierung nach int puts("false"); int intflag = flag; // implizite Int-Konvertierung // true=1, false=0 printf("intflag = %d\n", intflag); printf("!flag = %d\n", !flag); } thales$ a.out false iflag = 0 !flag = 1 Zur Erinnerung: Integerwerte == 0 werden in C als false, Integerwerte != 0 als true bewertet. C++ bewertet Bedingungen ("x>5") jetzt als echte Boolean --- in der Praxis ergeben sich aber durch die impliziten Typumwandlungen zwischen Boolean und Integer keine Unterschiede zu C.

3.3.8.2 Änderung beim Aufzählungsdatentyp " enum"

Aufzählungen können in C über den Datentyp enum (enumeration = Aufzählung) realisiert werden. Implizit werden Variablen des Datentyps enum als Integerwerte behandelt und man konnte auf diese Variablen auch die üblichen Integeroperationen (++, -- etc.) anwenden. Das ist in C++ anders: thales$ cat ampel.c #include <stdio.h> int main() { enum ampel { gruen, gelb, rot } farbe; farbe = gruen; while (farbe<=rot) printf("farbe ist %d\n", farbe), // Typumwandlung enum->int farbe++; // (Zeile 8) } thales$ a.out farbe ist 0 farbe ist 1

Page 22: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-22

farbe ist 2 thales$ cp ampel.c ampel.cc thales$ gcc ampel.cc ampel.c c: In function `int main()': ampel.cc:8: no `operator ++ (int)' declared for postfix `++', trying prefix operator instead ampel.cc:8: no match for `++main()::ampel &'

Die Umwandlung der Enumvariable farbe in einen Integerwert wird von C und C++ unterstützt. Der C++-Compiler hingegen weigert sich, den Inkrementoperator ++ auf farbe anzuwenden. Eine einfache Abhil fe ist aber, farbe direkt als Integer zu deklarieren (int farbe=gruen; )

3.3.8.3 Konstanten Einfache Konstanten können in C und C++ als Makros (Textersatz!) via #define realisiert werden. Oder aber über Variablen, bei deren Deklaration das Schlüsselwort const vorangestellt wird. Das bewirkt dann, dass der Wert der Variablen zur Laufzeit des Programms sich nicht mehr verändern kann. Die Realisierung war innerhalb der C-Compiler war unterschiedlich. So gab der gcc bei Veränderung von const-Variablen lediglich eine Warnung aus. Die C++-Compiler sind hier genauer und brechen den Compili ervorgang ab: thales$ cat const.c const int x = 5; main() { x++; } thales$ gcc const.c const.c: In function `main': const.c:4: warning: increment of read - only variable `x' thales$ cp const.c const.cc thales$ gcc const.cc const.cc: In function `int main()': const.cc:4: increment of read - only variable `x' Konstante Variablen (welch ein Widerspruch!) können ihren Wert nicht verändern. const int x = 5; // hier ist x konstant!! double vektor[x]; // Deklaration ist ok - unter C manchmal nicht x++; // nicht erlaubt x = 8; // nicht erlaubt const char *s = "Hallo"; // Achtung! String ist konstant, Zeiger nicht! s++; // Ok - String bleibt ja unberührt s[0] = 's'; // nicht ok! String wird verändert! s="Warum?"; // erneute Zuweisung an s ist ok! Achtung: das "konstant" bezieht sich auf die Platzierung des Werts der Variablen im (unveränderlichen) Speicherbereich für Konstanten! Deshalb kann zwar der Charpointer s verändert werden (die eigentliche Variable ist im veränderlichen Speicherbereich; der Text auf den s zeigt liegt aber im konstanten Speicher), die Integervariable x hingegen nicht (die Speicherzellen für die

Page 23: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-23

Integer x liegen im konstanten Bereich und enthalten den Wert 5). Soll der Zeiger auch konstant sein, muss das Schlüsselwort direkt vor dem Variablennamen stehen: char * const s = "Hallo"; // Achtung! Zeiger ist konstant, String nicht! s[0] = 'k'; // Geht!! String ist nicht konstant s++; // Geht nicht!! Zeiger ist konstant const char * const s2 = "Hallo"; // String und Zeiger konstant s2[0] = 'o'; // Geht nicht!! String ist konstant s2++; / / Geht nicht!! Zeiger ist konstant Sinnvoll sind Konstanten z.B. auch in Funktionsdeklarationen. Hiermit kann angezeigt werden, dass der Wert des übergebenen Parameters in der Funktion nicht verändert wird: const char * strchr(const char * const s, const char ch); // weder der Speicherbereich, auf den s zeigt, noch s selbst oder ch // können verändert werden // der Speicherbereich, der von strchr zurückgeliefert wird, kann nicht // verändert werden

3.3.9 Kleine Einführung in das Exceptionhandling Das Problem: Es gibt immer wieder Funktionen, die dem aufrufenden Programm einen Fehlerfall signalisieren müssen. So liefert z.B. FILE *fopen() einen NULL-Pointer als Resultat zurück, wenn eine Datei zum Lesen geöffnet werden soll , die nicht existiert. Das ist einfach, da NULL kein regulärer Filepointer ist und damit nicht als reguläres Resultat der Funktion zurückkommen kann. Es gibt aber Fälle, bei denen der komplette Wertebereich der Funktionsresultate regulär ist: die Funktion float log(float f) liefert den (natürlichen) Logarithmus der Zahl f . Der Wertebereich des Logarithmus ist aber bekanntlich (-��� ����� 'DPLW� NDQQ� GHP� $XIUXIHU� NHLQ�

Resultat übermittelt werden, dass einen Fehlerfall anzeigt (log(-4) ist z.B. nicht definiert). Deshalb müßte in diesem Fall vor Aufruf der Funktion selbst sichergestellt weden, dass das Argument positiv ist. Eine andere Alternative wäre, dass die log()-Funktion intern eine (globale) Fehlervariable setzt, die nach dem Aufruf jedesmal überprüft werden kann und einen Fehlercode enthält (so wie z.B: int errno beim FileIO enthält). Oder aber, das Funktionsresultat von Log() ist vom Typ Boolean und das Ergebnis wird über einen Call -By-Reference-Parameter an den Aufrufer zurückgegeben: bool Log(float f, float &res) --- dann aber geht vieles von der Eff izienz des Aufrufs verloren, da das (eigentliche) Funktionsresultat nicht weiter in einem Statement verwendet werden kann: float zahl = 3.5, logzahl;

// berechne: logzahl = 3*Log(zahl) if (!Log(zahl, logzahl) puts("ein Fehler beim Logarithmus ist aufgetreten"), exit(1); logzahl = 3*logzahl; C++ (und z.B. auch Java) gehen einen anderen Weg über das sog. Exceptionhandling. Jede Funktion ist in der Lage über den throw-Operator eine(n) Ausnahme(-zustand) zu setzen (zu "werfen"). Dadurch wird die ausgeführte Funktion sofort beendet. Fängt die aufrufende Funktion die Exception nicht ab, wird die Exception an die nächste Funktion in der Aufrufhierarchie weiter-

Page 24: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-24

gegeben etc. Fängt main() als letzte Instanz in der Aufrufhierachie die Ausnahme nicht ab, wird das komplette Programm beendet, indem die Funktion terminate() aufgerufen wird. thales$ cat except.cc #include <stdio.h> #include <stdlib.h> #include <math.h> float Log(float exp) // natürlicher Log mit Exceptionhandling { if (exp<=0) { st atic char buf[100]; sprintf(buf, "log(%f) is undef!", exp);

throw buf; // throw exception and return } return log(exp); } int main(int argc, char **argv) { float erg; if (argc<2) return 1; try { // eine auftretende Exception beendet // s ofort diesen Block erg = 3*Log(atof(argv[1])); // kritischer Aufruf printf("log von %s ist %f \ n", argv[1], erg ); } catch (const char *s) { // Abfangen einer char* - exception - Meldung

printf("uups: %s \ n", s); exit(1); } catch (...) { // abfangen aller anderen exceptions puts("general catch!"); // dieser Block ist optional! } return 0; } thales$ a.out - 5 uups: log( - 5.000000) is undef! Die Syntax lautet: throw Expression; Expression kann hier eine Variable, eine

Konstante oder ein Objekt sein bzw. eine Funktion, die ein Objekt zurückliefert. Es kann dort alles stehen, bis auf den Datentyp void . Die aufrufende Funktion muss den kriti schen Aufruf in einen sog. try {} Block setzen und dann mit catch {} die Ausnahme abfangen. Wenn in einem Try-Block eine Exception auftritt, wird der Block sofort verlassen und der Code nach dem Block abgearbeitet. Nach einem Tryblock muss (syntaktisch) sofort ein Catchblock folgen. Der Catchblock wird über den Datentyp der Exception parametrisiert und kann so auf unterschiedliche Ereignisse unterschiedliche reagieren. Es gibt aber auch den "globalen" Catchblock (...) der alle Datentypen abfängt. Die Ansprungreihenfolge der Blocks ist in der Reihenfolge von "oben nach unten". Deshalb darf der globale (...) -Catchblock nie vor einem anderen stehen, da er alle Fälle abfängt und deshalb kein anderer (weiter untenstehender) Block mehr angesprungen wird.

Page 25: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-25

Es obliegt aber natürlich dem aufrufenden Programm, ob es die betreffende Funktion in einen Try-Catch-Block setzt oder ob sie das Fehlerhandling einer "höheren" Instanz überläßt.

3.3.10 Speichermanagement via new und delete Wenn man in C dynamisch zur Laufzeit Speicher benötigt, wird das über die beiden Funktionen calloc() bzw. malloc() realisiert. Mit free() wird Speicher wieder freigegeben. Bei void *calloc(size_t nelem, size_t elsize) muß man sowohl die Größe des Objekts (sizeof struct oder sizeof int ...) und die Anzahl der zu erzeugenden Objekte mit angeben. C++ ersetzt die Funktione calloc() und free() durch die Operatoren new und delete . Der Hauptgrund dafür wird in einem späteren Kapitel klar: beim Erzeugen neuer Objekte (aus Klassen) wird für jedes Objekt ein sogenannter Konstruktor (eine Funktion) und beim Löschen eines Objekts ein Destruktor aufgerufen. Würde nun Speicherplatz für Objekte via calloc() dynamisch erzeugt, dann wäre nicht automatisch sichergestellt , dass auch der Konstruktor für das Objekt aufgerufen wird, da calloc() nichts von der Beschaffenheit des Objekts (und damit nichts von der Konstruktorfunktion) weiß. thales$ cat new1.cc #include <stdio.h> int main() { int *p; // p ist Zeiger auf Integer p = new int; // Platz holen für eine Integer printf("nach Speicher holen: *p = %d \ n", *p); *p = 3; // 3 als Wert eintragen printf("Methode 1: *p = %d \ n", *p); delete p; // Integerspeicherplatz wieder freigeben p = new int(5); // Platz für Integer holen und mit 5 initialisieren printf("Methode 2: *p = %d \ n", *p); delete p; const int max = 4; p = new int[max]; // Platz für 4 Integerzahlen! for (int i=0; i<max; i++) p[i] = max - i; for (int i=0; i<max; i++) printf("p[%d]=%d \ n", i, p[i]); delete [] p; // IntegerVEKTOR freigeben, deshalb [] } thales$ a.out nach Speicher holen: *p = 543516788 Methode 1: *p = 3 Methode 2: *p = 5 p[0]=4 p[1]=3 p[2]=2

Page 26: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-26

p[3]=1 Anders als bei calloc() werden elementare Datentypen wie int, char ... bei der Speicherplatzbeschaffung via new nicht mit 0 vorinitialisiert! Die Initialisierung einer einzelnen Variablen kann über die Zahl in runden Klammern geschehen: p = new int(5); Folgendes ist zu beachten:

• Es dürfen an delete nur Objekte übergeben werden, die via new beschaff t wurden! • delete darf nicht zur Freigabe von Speicher verwendet werden, der via calloc/malloc

geholt wurde! • delete darf nicht zweimal (hintereinander) auf das gleiche Objekt angewendet werden! • Bei Freigabe von Vektoren, die mit new beschaff t wurden, ist delete explizit mit []

aufzurufen: p = new int[10]; ... delete [] p; • Es ist explizit erlaubt, delete mit einem Nullzeiger als Argument aufzurufen. Das hat

(anders als bei free() ) keinen (Neben-)Effekt. Falls new den angeforderten Speicher nicht beschaffen kann, gibt es standardmäßig eine Exception, die abgefangen werden sollte, es wird also nicht einfach ein Nullzeiger zurückgeliefert: try {

int *p = new double[20000000]; // that's really big, man } catch (…) { fputs("no more room for big guys \ n", stderr), exit(1); } Genau genommen wird beim Fehlschlagen der Speicherallokation eine Funktion aufgerufen, die via throw die Exception auslöst. Diese Funktion kann aber global via set_new_handler() geändert werden, damit es eine zentrale Stelle im Programm für Speicherprobleme gibt: #include <stdio.h> #include <new.h> void myhandler(void) { fputs("no more room for nobody ... \ n", stderr); exit(1); } i nt main() { int *ptr; void (*old_new_handler)(); // Zeiger auf alten Handler old_new_handler = set_new_handler(myhandler); for (int i = 0; i< 200000; i++) ptr = new int[9999999]; puts("ich hab hier viel Platz"); // probably never reached } thales$ a.out no more room for nobody ...

Page 27: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-27

3.3.11 Namensräume Neu ist die Möglichkeit, Variablen und Funktionen sog. Namensräumen zuzuordnen. Namensräume

tragen - bis auf die Ausnahme des hier nicht behandelten anonymen Namensraums - Bezeichnungen, durch die die Funktionen und Variablen näher spezifiziert werden können. Der Trenner zwischen Bezeichner des Namensraums und Namen der Funktion/Variable ist der ":: ". Durch diese Konstruktion kann z.B. erreicht werden, dass bisher "vergebene" Standardnamen (wie z.B: fopen() , read() etc.) vom Programmierer in einem neuen Namensraum als eigene Funktionsnamen verwendet werden kann.

thales$ cat names.cc #include <stdio.h> // Einschalten Namensraum spec1 namespace spec1 { int flag = 1; int mypi = 314; void test() { puts("Test - Fkt. aus Spec1"); } } // Namensraum spec2 namespace spec2 { int flag = 2; void test() { puts("Test - Fkt. aus Spec2"); } } // "globaler" Namensraum ab hier int global = 5; void test() // Testfkt. 1 { puts("Test - Fkt. aus ''globalen'' Namensraum"); } void t est(int x) // Testfkt. 2 { printf("Test - Fkt glob. Namensraum: x = %d \ n", x); } using namespace spec1; // Namensraum spec1 "fluten" ("IMPORT spec1;")

Page 28: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-28

int main() { printf("mypi is %d\n", mypi); // aus Namensraum spec1 printf("flag is %d\n", flag); // aus Namensraum spec1 printf("global is %d\n", global); // aus globalem Namensraum spec1::test(); // aus spec1 spec2::test(); // aus spec2 ::test(); // :: verweist explizit auf glob. NR // sonst Verwechslung mit spec1::test() test(3); // globales Test --- Overloading!! return 0; }

Im globalen Namensraum eines Moduls existieren alle Variablen und Funktionen, die nicht näher einem anderen Namensraum zugeordnet werden (Dadurch wird u.a. Abwärtskompatibilit ät zu alten C-Programmen gewährleistet.) Mit dem Kommando using namespace spec1; werden die Deklarationen des Namensraums spec1 in den globalen Namensraum eingefügt. Dadurch können Funktionen und Variablen (ausser bei Namenskolli sionen wie z.B. test()) aus spec1 ohne weiteren Bezeichner des Namensraums aufgerufen werden. Um nur eine einzelne Variable (oder Funktion) aus einem Namensraum bekannt zu machen, schreibt man z.B. using spec1::mypi; Funktionen der C-Standardbibliothek (fopen, fclose, strchr, strcpy ...) werden von den neueren C++-Compilern im Namensraum std abgelegt. Üblicherweise geschieht das in den Headerdateien stdio.h, string.h: #ifdef __cplusplus namespace std { #endif ..... #ifdef __cplusplus } using namespace std; // fluten des globalen Namensraums #endif Bei vielen Implementierungen dieser Headerdateien wird zum Schluß der globale Namensraum noch mit den Deklarationen aus std "geflutet", so dass es wiederum bei der Abwärtskompatibilit ät zu C bleibt. Der gcc version 2.95.3 20010315 macht das genau so --- neuere Versionen des gcc überlassen dies jedoch dem Benutzer. Zu jeder Headerdatei für die C-Standardbibliothek existiert ein originäres C++-Headerfile, das ohne Endung .h und mit einem vorangestellten c.... benannt ist: #include <cstdio> statt #include <stdio.h>.

• Manche Compiler haben vorkompilierte Headerfiles --- das fehlende ".h" deutet darauf hin, dass die Informationen des Headerfiles nicht unbedingt in einer Datei mit der Endung ".h" stehen muss, sondern z.B. in einer gesonderten Datenbank.

• Es steht dem C++-Compiler frei, ob er bei der Aufforderung #include <cstdio> einfach die (auf C++ bezüglich Namensraum, extern "C" etc. abgestimmte) Headerdatei stdio.h lädt.

Page 29: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-29

3.3.12 Ein Beispiel zu Namensräumen und Modularisierung Das folgende Beispiel verdeutlicht die Benutzung von Namensräumen in verschiedenen Modulen: thales$ cat makefile obj=main.o mod1.o mod2.o main: $(obj) gcc -o main $(obj) main.o: mod1.h mod2.h mod1.o: mod1.h mod2.o: mod2.h thales$ cat mod1.h #ifndef MOD1 #define EXTERN extern #else #define EXTERN #endif namespace mod1 { EXTERN int anzahl; void func1(); } thales$ cat mod2.h #ifndef MOD2 #define EXTERN extern #else #define EXTERN #endif namespace mod2 { EXTERN int anzahl; void func1(); }

Page 30: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-30

thales$ cat mod1.cc #include <stdio.h> #define MOD1 #include "mod1.h" void mod1::func1() { puts("func1 in mod1 called"); mod1::anzahl++; } thales$ cat mod2.cc #include <stdio.h> #define MOD2 #include "mod2.h" void mod2::func1() { puts("func1 in mod2 called"); mod2::anzahl++; } thales$ cat main.cc #include <stdio.h> #include "mod1.h" #include "mod2.h" int main(int argc, char **argv) { puts("in main"); mod1::func1(); printf("anzahl = %d\n", mod1::anzahl); mod2::func1(); mod2::func1(); printf("anzahl = %d\n", mod2::anzahl); }

Page 31: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 3-31

Inhalte von Headerfiles [Stroustrup00]: Bekannte Namensbereiche namespace N { /* ... */ }

Typdefinitionen struct Position { int x, y; }

Template-Deklarationen template<class T> T max(T x[], int len);

Template-Definitionen template<class T> T max(T x[], int len){ /* … */ }

Funktionsdeklarationen extern int strlen(const char *);

Inline-Funktionsdefinitionen inline char get(char *p) { return *p++; }

Datendeklarationen extern int a;

Konstantendefinitionen const float pi = 3.1415926535;

Aufzählungen enum Ampel { rot, grün, ge lb };

Namensdeklarationen struct Matrix;

weitere Include-Anweisungen #include <pascal.h>

Makrodefinitionen #define VERSION 1.2

bedingte Übersetzung #ifdef __cplusplus

Was sollte nicht in Headerfiles stehen: Normale Funktionsdefinitionen char get(c har *p) { return *p++; }

Datendefinitionen int a;

Vektordefinitionen short tbl[] = { 1, 2, 3 };

Page 32: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-32

4 Datenabstraktionen

4.1 Klassen Bisher konnte man in C nur durch besondere "Klimmzüge" neue Datentypen schaffen, nämlich durch den Umweg über typedef und st ruct : typedef struct { // Struktur zur Repräsentierung eines int zaehler; // Dezimalbruchs

int nenner; } BRUCH; Durch diese Definition wird z.B. der neue Datentyp BRUCH geschaffen. Leider ist aber der Umgang mit selbst geschaffenen Datentypen nicht so einfach wie mit den fundamentalen Typen: BRUCH a = {5, 3}, // Initialisierung

b = {1, 2}, c; // a = fuenf Drittel und b = einhalb �

//c = a+b; // schoen waer's! das geht noch nicht! c = addbruch(a, b); // Aufruf einer speziellen Fkt. f. Addi tion mit BRUCH addbruch(BRUCH a, BRUCH b) { BRUCH c;

c.zaehler = a.zaehler * b.nenner + b.zaehler * a.nenner; c.nenner = a.nenner * b.nenner; // verwirrend ... aber richtig return c; // liefert Kopie der Ergebnisstruktur

} C kennt also "neue Datentypen", setzt das Konzept aber bei weitem nicht konsequent um: Es können u.a. keine Operatoren für neue Datentypen definiert werden. Genau betrachtet wird nur ein neuer Name für einen bestehenden Datentyp (oder eine Struktur) angelegt. In C++ (und anderen OO-Sprachen) dagegen dient das Klassenkonzept zur einfachen und umfassenden (z.B. auch bezüglich Operatoren!) Definition von neuen Datentypen. Eine Klasse kann somit einfach als neuer, benutzerdefinierter Datentyp angesehen werden.

4.1.1 Elementfunktionen Zu dem obigen Beispiel für den Dezimalbruch könnte man nun sukzessive eine Reihe von Funktionen definieren, die die Basisoperationen auf Brüchen repräsentieren: BRUCH addiere(BRUCH a, BRUCH b); BRUCH subtrahiere(BRUCH a, BRUCH b);

Page 33: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-33

void invertiere(BRUCH &a); void kuerze(BRUCH &a); /* etc. */ In C++ ist es möglich, diese Funktionen als sog. Elementfunktionen mit in die Struktur aufzunehmen (was in C syntaktisch nicht erlaubt ist). Allerdings ist hier ein typedef nicht mehr nötig: struct BRUCH {

int zaehler, nenner; BRUCH addiere(BRUCH a, BRUCH b); BRUCH subtrahiere(BRUCH a, BRUCH b); void invertiere(BRUCH &a); void kuerze(BRUCH &a); /* etc. */

}; Die Strukturdefinition ist ein Sonderfall einer Klassendefinition in C++. Die oben deklarierten Funktionen können nur mehr (wie auch die normalen Variablen) über den bekannten "."-Auswahl-operator aufgerufen werden: BRUCH a; a.invertiere(a); // Aufruf ok invertiere(a); // Funktion invertiere unbekannt; Fehler Weil es natürlich mehrere Strukturen mit einer Elementfunktion invertiere geben kann, muß der Strukturnamen bei der Definition der Funktion mit angegeben werden: void BRUCH::invertiere(BRUCH &x) { int hlp; hlp = x.nenner; x.nenner=x.zaehler; x.zaehler=hlp; } Der doppelte Doppelpunkt "::" erinnert (zurecht!) an die Namensräume in C++. Innerhalb der Elementfunktion kann bei den (Element-)Variablen der Struktur auch der Name der Variable weggelassen werden, da er ja beim Aufruf der Funktion spezifiziert wird. Auf die Elementvariablen kann dann direkt zugegriffen werden: void BRUCH::invertiere() { int hlp; hlp = nenner; nenner = zaehler; zaehler = hlp; } int main() { BRUCH a;

// ... a.invertiere(); // Variable a ist spezifiziert // ...

}

Page 34: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-34

Unmerklich haben wir eine am Objekt orientierte Umstellung des Aufrufs erhalten. In prozeduralen Programmiersprachen orientiert sich das Geschehen an Prozeduren, in OO-Sprachen an Objekten (als Repräsentierung einer Klasse): BRUCH c; invertiere(&c) ; // so in klassischem C c.invertiere(); // so in C++

4.1.2 Klassendefinitionen und Zugriffskontrolle Das C++-Konstrukt class BRUCH { // ... } wird Klassendefinition genannt. Strukturdefinitionen sind Spezialfälle von Klassen, bei denen sämtliche Elemente (Variablen und Funktionen) öffentlich sind. struct BRUCH {

int zaehler, nenner; BRUCH addiere(BRUCH a, BRUCH b); BRUCH subtrahiere(BRUCH a, BRUCH b); void invertiere(); void kuerze(); /* etc. */

}; ist äquivalent zu class BRUCH { public:

int zaehler, nenner; BRUCH addiere(BRUCH a, BRUCH b); BRUCH subtrahiere( BRUCH a, BRUCH b); void invertiere(); void kuerze(); /* etc. */

}; Das Schlüsselwort public (und sein Gegenpart private ) regeln den Zugriff auf die Elemente einer Klasse von außen: Auf Public-Elemente kann jede beliebige Funktion lesend und schreibend zugreifen, auf Private-Elemente weder lesend noch schreibend. Hier muss der Zugriff über Elementfunktionen der Klasse erfolgen: class BRUCH { private:

int zaehler, nenner; // Zaehler+Nenner nach außen verborgen public: // Funktionen bekannt

Page 35: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-35

BRUCH addiere( BRUCH a, BRUCH b); BRUCH subtrahiere(BRUCH a, BRUCH b); void invertiere(); void kuerze(); /* etc. */

}; bei der obigen Art der Definition ist der folgende Zugriff unzulässig: int main() { BRUCH a; a.nenner = 5; // Fehler! Element geschützt = "private" } Elementfunktionen einer Klasse können nach wie vor auf deren private Variablen zugreifen: void BRUCH::invertiere() // Implementierung wie gehabt! { int hlp;

hlp = nenner; nenner = zaehler; zaehler = hlp; }

4.1.3 Der Konstruktor Es ist das gute Recht einer Klasse, dass manche Variablen (und auch einzelne Elementfunktionen) "privat" und damit nicht von aussen manipulierbar sind. Genau diese Abschirmmöglichkeit unterstützt die gewünschten Konzepte der "Kapselung" und des "Information-Hiding". Das Abschirmen wirft aber die Frage auf, wer dann für die Initialisierung dieser Variablen sorgt, falls sie nicht für jedes Objekt nach der Erzeugung identische Werte haben sollen. Wir haben weiter oben schon gesehen, dass die Elementfunktionen einer Klasse sehr wohl in der Lage sind, auch auf private Variablen lesend und schreibend zuzugreifen. Es gibt nun eine ausgezeichnete Elementfunktion, die die notwendigen Initialisierung für die Objekte übernimmt: der sogenannte Konstruktor. Diese Elementfunktion trägt den gleichen Namen wie die Klasse selbst und hat keinen Rückgabewert, nicht einmal void : class BRUCH { private:

int zaehler, nenner; // Zaehler&Nenner nach außen verborgen public: // Funktionen bzw. ggf. Daten bekannt

BRUCH addiere(BRUCH a, BRUCH b); //... BRUCH(int initzaehler, int initnenner); // Konstruktor

}; BRUCH::BRUCH(int initzaehler, int initnenner) { nenner = initnenner; zaehler = initzaehler; } Es gelten für den Konstruktor natürlich die gleichen Regeln bezüglich Overloading und vorinitialisierten Parametern, wie für andere Funktionen auch (siehe 3.3.5 und 3.3.3). Der folgende Konstruktor würde ohne Angabe von Parametern einfach den Bruch mit 0 = 0 / 1 sinnvoll vor-initialisieren.

Page 36: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-36

BRUCH::BRUCH(int initzaehler=0, int initnenner=1) { nenner = initnenner; zaehler = initzaehler; } Der Aufruf des Konstruktors geschieht für jedes Objekt automatisch beim Anlegen des Speicherplatzes für das Objekt: BRUCH a; // Aufruf des Standardkonstruktors mit (0 ,1) BRUCH b(1, 2); // Aufruf des Standardkonstruktors mit (1,2) BRUCH *c; // hier KEIN Aufruf des Konstruktors, da c Zeiger ist c = new BRUCH(1,1); // jetzt Aufruf des Standardkonstruktors mit (1,1) c = new BRUCH[10]; // c ist nun Vektor von 10 Brüchen ( 0,1) Die Implementierung der Klassenmethoden (z.B. des Konstruktors) kann direkt in der Klassen-definition erfolgen oder später außerhalb. Geschieht die Implementierung innerhalb der geschweiften Klammer der Klassendefinition, dann versucht der Compiler, die Funktion inline zu implementieren: class BRUCH { private:

int zaehler, nenner; public:

//der Konstruktor ist implizit inline BRUCH(int initzaehler, int initnenner) { nenner = initnenner; zaehler = initzaehler; }

}; Soll eine Klassenmethode ausserhalb der Klassendefinition implementiert werden, dann ist vor den Namen der Methode der Name der Klasse - getrennt durch :: - zu stellen. Die Implementierung außerhalb der eigentlichen Klassendefinition empfiehlt sich aufgrund des Informationhidings, da die eigentliche Klassendefinition jedem Programm (via include) als Schnittstelle zur Verfügung gestellt werden muss.

4.1.4 Der Destruktor Alles Schöne geht einmal zu Ende - so auch das Leben als Objekt: lokale Objekte leben als lokale "Variablen" zur Laufzeit einer Prozedur und werden am Ende der Prozedur wieder zerstört. Objekte, die als Vektoren via new angelegt wurden, können zur Laufzeit mit delete wieder gelöscht werden. Bei jeder Freigabe eines Objektes wird die sog. Destruktormethode der Klasse aufgerufen: class BRUCH { private:

int zaehler, nenner; public:

BRUCH(int initzaehler, int initnenner) // Konstruktor { nenner = initnenner; zaehler = initzaehler; } ~BRUCH() { /* ... */ } // Destruktor

};

Page 37: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-37

Destruktormethoden sind vor allem dann notwendig, wenn bei der Erzeugung des Objektes Speicherplatz via new für einzelne Elemente beschaff t wurde: thales$ cat studi.cc #include <stdio.h> class student { char *name; // nur Platz für den Pointer, nicht für den Namen long matrikelnr; // hier ist auch Pl atz für die Matrikelnummer da public:

student(char *init, long mnr=0L) { // Konstruktor holt Speicher & kopiert

printf("erzeuge Student %s \ n", init); name = new char[strlen(init)+1]; strcpy(name, init); matrikelnr = mnr;

} ~student() { // Destruktor gibt Speicher wieder frei

printf("lösche Student %s \ n", name); delete [] name;

} }; int main() { Student peter("Peter Muessig", 123456L); } thales$ studi erzeuge Student Peter Muessig lösche Student Peter Muessig

4.1.5 Verweis auf das aktuelle Objekt in einer Elementfunktion: this Manchmal braucht man in einer Elementfunktion einen expliziten Verweis auf das komplette aktuelle Objekt, das die Elementfunktion aufruft. Auf die einzelnen Komponenten des Objekts kann man immer innerhalb der Funktion über die Variablennamen zugreifen, das Objekt als ganzes ist implizit durch den Zeiger this verfügbar: class student { /* wie in 4.1.4 */ bool zwilling(char *name) {

return !strcmp(name, this - >name); }

};

4.1.6 Der Kopierkonstruktor und der Operator = Am Beispiel der Studentenklasse wird klar, dass man beim Erzeugen von Objekten in die gleichen "Speicherfallen" tappen kann, die bereits von C her leidlich bekannt sind: Verwechseln von Zeigern (noch kein Speicherplatz angelegt!) und Vektoren (stellt bei der Definition Speicherplatz bereit!), Vorsicht bei Zeigerkopien, wenn Speicher bereits freigegeben wurde etc. Ein kriti scher Punkt in C++ ist die Zuweisung von Objekten, die dynamische Komponenten besitzen: /* class student wie in 4.1.4 */

Page 38: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-38

int main() { student a("Keiner"); student *b = new student("Peter", 55412l); a = *b; // Kopiere das Objekt, auf das b zeigt, nach a delete b; // danach ist a.name nicht mehr definiert !! } Beim Kopieren (z.B. bei Übergabe eines Objekts an eine Prozedur) und Zuweisen von Objekten wird per default eine 1:1 byteweise erstellte Kopie des Originals erstellt . Deshalb wird z.B. beim Kopieren von Zeigervariablen (hier: student::name ) nur die Adresse der Speicherfläche kopiert, auf die der Zeiger verweist. Wird der Speicher hinter dem Zeiger beim Zerstören eines Objektes freigegeben (hier durch delete b ), dann verweist die Zeigerkopie (hier: a.name ) auf eine nicht mehr definierte Speicherfläche. Eine Lösung aus dem Dilemma ist nur durch eine Neudefinition des Kopierkonstruktors und des Zuweisungsoperators für Elemente möglich: class student {

char *name; long matrikelnr; public: /* … */ student(student &); // Der Kopierkonstruktor student &operator=(student &); // Der Zuweisungsoperator

}; student::student(student &a) { name = new char[strlen(a.name)+1]; strcpy(name, a.name); matrikelnr = a.matrikelnr; } student &student::operator=(student &a) { name = new char[strlen(a.name)+1]; strcpy(name, a.name); matrikeln r = a.matrikelnr;

return *this; } (Komplettes Programm siehe unter 9.2 Lebenszyklus von Objekten)

4.1.7 Statische Klassenelemente thales$ cat studi2.cc #include <stdio.h> class student { char *name; // n ur Platz für den Pointer, nicht für den Namen static int anzahl; // Deklaration: Var. Anzahl Studentenobjekte public:

student(char *init) { // Konstruktor holt Speicher & kopiert printf("erzeuge Student %s \ n", init); name = new char[strlen(init)+1]; strc py(name, init); anzahl++;

} ~student() { // Destruktor gibt Speicher wieder frei

printf("loesche Student %s \ n", name); delete [] name; anzahl --;

Page 39: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-39

} static int getanzahl() { return student::anzahl; }

}; int student::anzahl = 0; // Definition: Var. Anzahl Studentenobjekte int main() {

printf("Anzahl Studis: %d \ n", student::getanzahl()); {

student peter("Peter Muessig"); // Objekt ist lokal zum Block printf("Anzahl Studis: %d \ n", student::getanzahl());

} printf("Anzahl Studis: %d \ n", student::getanzahl() ); } thales$ studi2 Anzahl Studis: 0 erzeuge Student Peter Muessig Anzahl Studis: 1 loesche Student Peter Muessig Anzahl Studis: 0 Statische Klassenelemente (Variablen und Funktionen) existieren unabhängig von den Objekten der Klasse: die Variable int st udent::anzahl wird genau einmal für die Klasse student angelegt und speichert im obigen Beispiel die Anzahl der erzeugten Studentenobjekte ab. Die Funktion int student::getanzahl() wiederum liefert den Wert dieser Variablen zurück. Die Funktion existiert unabhängig von bisher erzeugten Objekten der Klasse und kann daher auch ohne vorangestellten Objektnamen aufgerufen werden. Stattdessen wird der Name der Klasse mit :: getrennt vorgestellt , um die Funktion aufrufen zu können. Natürlich kann in statischen Klassenelementen nicht auf die Variablen der Klasse zugegriffen werden - mit Ausnahme natürlich von statischen Klassenvariablen.

4.1.8 Definieren bzw. Überladen von Operatoren Eine grosse Stärke von C++ ist die Möglichkeit, die Standardoperatoren für Objekte (neu) zu definieren, wie bereits in 4.1.6 gezeigt. Gegeben sei wieder die bisherige Klassendefinition für ganzzahlige Brüche: class BRUCH { private:

int zaehler, nenner; // Zaehler&Nenner nach außen verborgen public: // Funk tionen bzw. ggf. Daten bekannt

BRUCH add(BRUCH a); //... BRUCH(int initzaehler, int initnenner); // Konstruktor

}; Natürlich wäre es viel schöner, statt c = a.add(b) schreiben zu können: c = a + b Das kann durch Definieren des Operators + für die Klasse Bruch geschehen: class BRUCH { /* ... */

Page 40: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-40

public: // Funktionen bzw. ggf. Daten bekannt BRUCH(int initzaehler, int initnenner); // Konstruktor BRUCH operator+(BRUCH &a) // Operator + { BRUCH c(zaehler, nenner); // Kopie des aufrufenden Objektes c.nen ner *= a.nenner;

c.zaehler *= a.nenner; c.zaehler += a.zaehler*nenner; return c; // Kopie (!) von c als Resultat

} }; int main() { BRUCH a(1, 2), b(1, 4); BRUCH c = a + b ; // Aufruf: c = a.operator+(b); // ... } Siehe auch: "9.3 Achtung bei Operatorenfunktionen mit einer Referenz auf temporäre Objekte"!

4.1.9 Ein komplettes Beispielprogramm für ganzzahlige Brüche

thales$ cat makefile CC=g++ OBJ=main.o bruch.o teste: teste.o bruch.o g++ - o teste te ste.o bruch.o teste.o: bruch.h thales$ cat teste.cc #include "bruch.h" #include <stdio.h> #include <stdlib.h> // Aufruf: teste z1 n1 op z2 n2 int main(int argc, char **argv) { if (argc!=6) printf("usage: %s z1 n1 op z2 n2 \ n", argv[0]), exit(1); br uch a(atoi(argv[1]), atoi(argv[2])); bruch b(atoi(argv[4]), atoi(argv[5])); bruch c; switch (argv[3][0]) { case '+': c = a + b; break; case ' - ': c = a - b; break; case '/': c = a / b; break; case '*': c = a * b; break; default: printf("'%c' no valid operand \ n", argv[3][0]); exit(1);

Page 41: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-41

} cout << "c = " << c << endl;

} thales$ cat bruch.h #include <iostream> /* * Klasse fuer ganzzahlige Brueche */ class bruch { int zaehler, nenner; /* Variablen nur intern bekannt */ static int ggt(int, int); /* private Funktionen */ void shorten_bruch(); public: /* Funktionen=Methoden oeffentlich */ /* * erzeugt ein Bruchobjekt und initialisiert es mit Zaehler und Nenner * falls kein Initwert uebergeben wird, wird der Zaehler mit 0 und der * Nenner mit 1 initialisiert */ bruch(int z=0, int n=1); /* gibt ein erzeugtes Bruchobjekt wieder frei */ ~bruch(); /* * addiert zwei Brueche und erzeugt einen Bruch, der das Ergebnis traegt * die Operatoren +-* / werden hier "ueberladen" = neu definiert */ bruch operator+ (bruch &a); bruch operator- (bruch &a); bruch operator* (bruch &a); bruch operator/ (bruch &a); /* konvertiert einen String in einen Bruch */ bruch str2bruch(char *s); // Lesezugriff auf private Elemente int getzaehler() const { return zaehler; } int getnenner() const { return nenner; } }; // Ausgabefunktion fuer den Bruch - Nenner und Zaehler sind ja private ostream &operator<<(ostream &out, const bruch &a); thales$ cat bruch.cc /* * elementares Bruchrechnen */ #include "bruch.h" #include <stdlib.h> #include <strings.h> #include <stdio.h> /* erzeugt ein Bruchobjekt und initialisiert es mit Zaehler und Nenner */ bruch::bruch(int z=0, int n=1)

Page 42: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-42

{ zaehler = z; nenner = n; (*this).shorten_bruch(); } /* gibt ein erzeugtes Bruchobjekt wieder frei */ bruch::~ bruch() { #ifdef DEBUG cout << *this; puts(" ... killed"); #endif /* demo only - nothing to happen */ } /* addiert zwei Brueche und erzeugt einen Ergebnisbruch */ bruch bruch::operator+(bruch &a) { bruch c(zaehler, nenner); c.nenner *= a.nenner; c.zaehler *= a.nenner; c.zaehler += a.zaehler*nenner; c.shorten_bruch(); return c; } /* subtrahiert zwei Brueche und erzeugt einen Ergebnisbruch */ bruch bruch::operator-(bruch &a) { bruch c(zaehler, nenner); c.nenner *= a.nenner; c.zaehler *= a.nenner; c.zaehler -= a.zaehler*nenner; c.shorten_bruch(); return c; } /* multipliz. zwei Brueche */ bruch bruch::operator*(bruch &a) { bruch c (zaehler, nenner); c.nenner *= a.nenner; c.zaehler *= a.zaehler; c.shorten_bruch(); return c; } /* dividiert zwei Brueche */ bruch bruch::operator/(bruch &a) { bruch c (zaehler, nenner); c.nenner *= a.zaehler; c.zaehler *= a.nenner; c.shorten_bruch(); return c;

Page 43: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-43

} /* * Berechnung des ggt der Absol utwerte zweier Zahlen nach Euklid (gaehn!) */ int bruch::ggt(int x, int y) { if (!x || !y) return 1; x = abs(x); y = abs(y); while (x!=y) { if (x>y) x - = y; else y - = x; } return x; } /* kuerzt einen Bruch */ void bruch::shorten_bru ch() { int x = ggt(zaehler, nenner); if (x>0) { nenner /= x; zaehler /= x; } if ((nenner<0) && (zaehler<0)) { nenner = abs(nenner); zaehler = abs(zaehler); } } /* * konvertiert einen String in einen Bruch */ bruch bruch::str2bruch(char *s) { char *pos = strchr(s, '/'); int z, n; if (pos) { *pos = 0; n = atoi(pos+1); } else n = 1; z = atoi(s); bruch res(z, n); res.shorten_bruch(); return res; }

Page 44: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-44

/* * gibt den Bruch (und nur diesen!) am Bildschirm aus */ ostream &operator<<(ostream &out, const bruch &a) { out << a.getzaehler(); if (a.getnenner()!=1) out << "/" << a.getnenner(); return out; }

4.1.10 Ein Freund fürs Leben: friend

Im obigen Beispiel mußten einzig und allein für den Ausgabeoperator << zwei Elementfunktionen geschaffen werden, die den (Lese-)Zugriff auf die privaten Daten der Klasse regelten: int getzaehler() und int getnenner(). Dies ist natürlich nicht schön, weil dadurch jede andere Funktion nun auch ein Lesezugriff auf die Daten hat - was nicht immer erwünscht ist. Es gibt noch einen zweiten Weg in C++, Lese- (und gleichzeitig auch Schreibzugriff) auf private Elemente an bestimmte andere Funktionen zu erteilen: Das Schlüsselwort hierzu heißt friend. Dadurch sieht die Klassendefinition wie folgt aus (getzaehler() und getnenner() sind nicht mehr nötig): class bruch { // wie oben ... friend ostream &operator<<(ostream &out, const bruch &a); };

Das führt dann zu einer neuen Implementierung von operator<<(): ostream &operator<<(ostream &out, const bruch &a) { out << a.zaehler; // Zugriff aus private-Komponente erlaubt! if (a.nenner!=1) out << "/" << a.nenner; return out; }

4.1.11 Liste überladbarer Operatoren

+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- ->* , -> [] () new new[] delete delete[]

Nicht überdefinierbar sind :: . und .* (aus [Stroustrup00]).

Page 45: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-45

4.1.12 Operatoren für unterschiedliche Datentypen Selbstverständlich können bei der Neudefinition eines Operators die zwei Operanden von unterschiedlichem Datentyp sein. So kann beispielsweise auch zu einem Bruch aus 4.1.9 eine Integer hinzuaddiert werden: // addiert eine ganze Zahl zu dem Bruch hinzu bruch bruch::operator+(int num) { bruch b(n um); // Bruch mit Zähler = num, Nenner = 1 return (*this)+b; } Beispiel: bruch a(1,2); a = a+5;

4.2 Initialisierung von Objekten über den " :" hinter der Konstruktor funktion Ein Beispiel hierfür findet sich im Anhang

4.3 Ein Template für sichere dynamische Vektoren Eines der "Lieblingsphänomene" von C findet man natürlich auch noch in C++: Unbeachtete Array-grenzen beim Schreibzugriff f ühren ggf. unbemerkt zu einer Datenmanipulation bei anderen Variablen: thales$ cat overwrite.cc #include <iostream > int main() { char s[] = "Hallo - ist da wer?"; int x[20]; // Arraygrenzenverletzung x[20] = ~0; // 4 Bytes, alle Bits auf 1 (entspricht - 1) cout << s << endl;// Na - wer ist denn da? } thales$ a.out ÿÿÿÿo - ist da wer? Der Wunsch nach "sicheren" Vektoren liegt nahe. Sicher in Bezug auf eine Überprüfung der Arraygrenzen bei Schreib- und Lesezugriffen. Das folgende Template für Vektoren aller Art schaff t Abhil fe - und macht gleichzeitig aus den statischen Vektoren dynamisch wachsende:

Page 46: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-46

thales$ cat vek.h // Templateklasse fuer "sichere", dynamische Vektoren #include <iostream.h> template<class T> class vektor { T *vek; // Der eigentliche Datenvektor int len; // Laenge des Vektors public: // generelle Fehlerklasse fuer Vektoren class error { public: char *msg; // Fehlertext char *method; // Name des Moduls error(char *message, char *fkt) { // Fehlerkonstruktor msg = message; method = fkt; } }; vektor(int i) { // Konstruktor vek = new T[i]; len = i; } vektor(vektor const &v) // Kopierkonstruktor { vek = new T[v.len]; memcpy(vek, v.vek, v.len*sizeof(T)); len = v.len; } ~vektor() { // Destruktor if (len == 0) return; delete [] vek; len = 0; } vektor operator=(vektor const &v) // Zuweisung { vek = new T[v.len]; memcpy(vek, v.vek, v.len*sizeof(T)); len = v.len; return *this; } T &operator[](int i) { // Zugriffsoperator [i] #ifdef DYNAMIK // Vektor automatisch vergroessern if (i>=0 && i>=len) resize(i+1); #endif if (i>=0 && i< len) return vek[i]; else throw error("out of range", "operator[]"); } void resize(int newlen) // Aenderung der Laenge des Vektors { if (newlen<=0) throw error("invalid len", "resize"); T *newvek = new T[newlen];

Page 47: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-47

memcpy(newvek, vek, len*sizeof(T)); delete [] vek; vek = newvek; len = newlen; } vektor &select(int von, int bis) // Auswahl eines Teilbereichs { if (von>bis || bis >=len || von <0) throw error("wrong range", "select"); vektor *tmp = new vektor(bis-von+1); // neuer Ergebnisvektor for (int i = von; i<= bis; i++) tmp->vek[i-von] = vek[i]; return (*tmp); } int length() { return len; } // Laenge des Vektors friend ostream &operator<< <T>(ostream &out, const vektor<T> &a); }; // Ausgabefunktion eines kompletten Vektors - via outstream werden alle // Elemente durch Blank getrennt ausgegeben template <class T> ostream &operator<< (ostream &out, const vektor<T> &a) { for (int i=0; i<a.len-1; i++) out << a.vek[i] << " "; out << a.vek[a.len-1] ; return out; } thales$ cat vek.cc //#define DYNAMIK #include "vek.h" int main() { try { vektor <double>m(20); // double Vektor der Laenge 20 m[5] = 12; cout << "m[5]=" <<m[5]<<endl; vektor <double>k = m.select(2,5); // Auswahl cout << "Vektor k der Laenge "<<k.length() <<": "<<k<<endl; m.resize(6); // m auf Laenge 6 kuerzen cout << "Vektor m der Laenge "<<m.length() <<": "<<m<<endl; cout << "m[7]=" <<m[7]<<endl; // Zugriff ausserhalb Grenzen } catch (vektor<double>::error err) { // Fehlermeldung der Vektoren? cerr <<"Error: "<<err.msg<<" in: "<<err.method<<endl; } catch (...) { cerr <<"General catch" <<endl; // Ausgabe auf Stderr } }

Page 48: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 4-48

thales$ a.out m[5]=12 Vektor k der Laenge 4: 0 0 0 12 Vektor m der Laenge 6: 0 0 0 0 0 12 m[7]=Error: out of range in: operator[] Wenn man beim Kompili eren von vek.cc den Makro DYNAMIK definiert, dann erhält man einen zur Laufzeit dynamisch wachsenden Vektor (Achtung: laufzeitinensiv - aber bequem - ): thales$ a.out m[5]=12 Vektor k der Laenge 4: 0 0 0 12 Vektor m der Laenge 6: 0 0 0 0 0 12 m[7]=0

4.4 Begr iffsklärung und Definitionen Der Gebrauch der Begriffe Klasse, Objekt, Instanz etc. ist in der OO-Welt nicht einheitli ch. Das vorliegende Skript übernimmt vorerst weitestgehend die technischen Sichtweise aus [Stroustrup00]:

• Eine Klasse ist ein benutzerdefinierter Datentyp, der ggf. neben Attributen auch Elementfunktionen bereitstellt:

o class student {

char *name; long matrikelnr; public: /* …*/ student() {/*..*/}; void printstudi() { /*...*/ }; };

• Ein (instanziiertes) Objekt (bzw. Instanz) ist ein konkretes Datenobjekt im Speicher, dass vom Datentyp der definierenden Klasse ist.

o student a("Klaus Mistner", 123412); • Ein Attribut einer Klasse ist eine in der Klasse definierte Variable (einfacher Datentyp oder

wieder ein Objekt). o a.matrikelnr;

• Eine Elementfunktion ist eine in einer Klasse definierte Funktion, die (damit) Zugriff auf alle Elemente der Klasse hat.

o a.printstudi();

• Eine Methode ist eine Elementfunktion. • Eigenschaften einer Klassen sind Methoden, die den Zugriff auf private Attribute regeln.

(Gelegentlich werden die Attribute auch global zu den Eigenschaften mit hinzu gezählt). • Eine Operation auf einem Objekt entspricht dem Aufruf einer Elementfunktion für das

entsprechende Objekt. • Eine Botschaft an ein Objekt senden entspricht dem Aufruf einer (ggf. virtuellen)

Elementfunktion des Objekts. • Die Schnittstelle (Interface) eines Objekts besteht aus sämtlichen Public-Attributen und

Public-Elementfunktionen der Objektklasse. o class student {

public: /* …*/ student() {/*..*/} void printstudi() { /*...*/ }

};

Page 49: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-49

5 Nützliche Hil fsmittel in C++

5.1 Str ings Ein String ist eine Sequenz von Zeichen. Der Datentyp string der Standardbibliothek bietet eine Reihe von Operationen zur Stringbearbeitung: Indexzugriff , Zuweisung, Vergleich, Anfügen und Suche nach Teilstrings. Er ist eine Instanziierung des Templates basic_string für den Datentyp char : typedef basic_string<char> string; Um die strings nützen zu können, muss #include <string> im Programm eingefügt werden. Hier steht auch der folgende Datentyp, der zur Darstellung von Positions und Längenangaben dient: typedef unsigned int size_t; typedef size_t size_type; Die Strings besitzen ein Public-Datenelement: static const size_type npos = - 1; // - 1 =maximaler Wert der Variable, // falls der echte Wert unbekannt Wer sich das String-Headerfile anschauen will und nicht weiß, wo es auf dem Rechner steht, kann sich mit einem einfachen Trick behelfen: thales$ cat trick.cc #include <string> thales$ g++ - E trick.cc|more Wichtige Konstruktoren [Pr inz98]: string() Erzeugt einen String der Länge 0 string(const char *s) Erzeugt einen String, der mit dem C-String s

initialisiert wird. string(const char *s, size_type n) Erzeugt einen String, der mit den ersten n

Zeichens des C-Strings s initialisiert wird. string(size_type n, char c) Bsp: string a(3, 'b'); // a = "bbb"

Erzeugt einen String, der genau n mal den Char c enthält.

string(const string& str, size_type pos=0, size_type n = npos) Bsp: string a("klaus"); string b(a, 2, 1); // b = "a"

Erzeugt einen String, der mit n Zeichen aus dem String str ab der Position pos initialisiert wird. Hierbei wird höchstens bis zum letzten Zeichen von str kopiert.

Page 50: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-50

Exceptions: class out_of_range; #include <stdexcept>

eine unzulässige Positionsangabe in einem String löst eine Exception vom Typ out_of_range aus (z.B. Methode string.at())

class length_error; #include <stdexcept>

Falls ein String wegen einer zu großen Länge nicht darstellbar ist, wird eine Exception length_error geworfen (z.B. beim Zusam-menfügen zweier Strings)

Methoden für den Elementzugr iff : char& operator[](size_type pos); Liefert eine Referenz auf das Element des

Strings in der Position pos, falls pos <size(), und gibt es zurück. Gibt das Stringende-Zeichen zurück, falls pos==size(). Falls pos>size, dann ist das Verhalten undefiniert. Nach einem anschließenden Schreibzugriff auf den String oder einem Aufruf von c_str() bzw. data() ist die Referenz undefiniert.

const char& at(size_type pos) const; Ähnliches Verhalten wie operator[] const, nur wird bei pos>=size() eine Exception vom Typ out_of_range erzeugt.

char &at(size_type off); s.o. Elementfunktionen zur Bestimmung der Länge: size_type size() const; size_type length() const;

Liefern beide die aktuelle Anzahl der im String gespeicherten Zeichen

bool empty(); Überprüfen auf leeren String. size_type max_size() const; maximal mögliche Länge des Strings size_type capacity(); maximal mögliche Stringlänge, ohne dass neuer

Speicher allokiert werden muß string.size() � ������������ ������� ���������� � string.max_size()

Elementfunktionen zur Änderung von Länge und Kapazität void resize(size_type n, char c); • n>max_size(): es wird die Exception

length_error ausgelöst • n < size(): der String wird auf die Länge

n gekürzt. • n > size(): der String wird ab Position

size() mit dem Zeichen c aufgefüllt . void resize(size_type n); Wie oben - der String wird jedoch mit 0Bytes

aufgefüllt .

Page 51: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-51

void erase(); Löscht alle Zeichen und setzt die Stringlänge auf 0.

void reserve(size_type n); Nimmt eine Änderung der capacity des Strings vor, falls n >capacity().

Zuweisungs-Operatoren - Parameter für string &operator=(parameter): const string& str; Weist den String str zu. Falls * this != str gilt ,

wird der ursprünglich durch *this kontrolli erte String durch einen String ersetzt, in den die Zeichen von str kopiert werden. Falls * this == str gilt , geschieht nichts.

const char *s; Weist den C-String s dem String *this zu. Hierbei wird der ursprünglich durch *this kontrolli erte String durch einen String ersetzt, in den die Zeichen von s kopiert werden.

char c; Weist das Zeichen c dem String *this zu. Hierbei wird der durch *this kontrolli erte String durch einen String der Länge 1 ersetzt, in den das Zeichen c kopiert wird.

Zuweisungs-Methoden string& assign(const string& str) Ersetzt den String *this durch einen String, in

den die Zeichen von str kopiert werden string& assign( const string& str, size_type pos, size_type n);

Ersetzt den String *this durch einen String der Länge n, dessen Elemente aus str ab der Position pos kopiert werden. Es wird höchstens bis zum letzten Zeichen von str kopiert.

string& assign( const char* s, size_type n);

Ersetzt den String *this durch einen String, dessen Elemente die ersten n Zeichen des C-Strings s sind. Es wird höchstens bis zum letzten Zeichen von s kopiert.

string& assign( const char* s); Ersetzt den String *this durch einen String, in den die Zeichen des C-Strings s kopiert werden

string& assign(size_type n, char c); Ersetzt den String *this durch einen String der Länge n, der mit dem Zeichen c aufgefüllt wird.

Page 52: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-52

Methoden zur Stringkonvertierung: size_type copy(char *s, size_type n, size_type pos=0);

Kopiert n Zeichen des Strings *this, beginnend ab der Position pos in der durch s adressierten char-Vektors. Es werden höchstens bis zum letzten Zeichen von *this kopiert. Returnwert: Anzahl kopierter Zeichen.

const char* c_str() const; Liefert die Zeichen des Strings *this als C-String der Länge size()+1, also mit Stringende-Zeichen. Returnwert: Der C-String (Read-Only).

const char* data() const; Liefert die Anfangsadresse der gespei-cherten Zeichenfolge. Diese ist nicht notwendig mit dem Stringende-Zeichen abgeschlossen. Returnwert: Read-Only Zeiger auf den Puffer.

Methoden zur Stringmanipulation string& insert(size_type pos, const string& str);

Fügt den String str bei der Position pos in den String *this ein. Hierbei wird der String *this durch einen String ersetzt, dessen erste Zeichen eine Kopie der ersten pos Zeichen des ursprünglichen Strings sind. Die nächsten str.size() Zeichen werden aus str kopiert und die restlichen Zeichen sind eine Kopie der verbleibenden Zeichen des ursprünglichen Strings.

string& insert(size_type pos, const char* s);

Fügt den C-String s bei der Position pos in den String *this ein.

string& insert(size_type pos1, const string& str, size_type pos2, size_type n);

Fügt bei der Position posl von *this n Zei-chen aus dem String str, beginnend bei Posi-tion pos2, in den String *this ein. Es wird höchstens bis zum letzten Zeichen von str kopiert.

string& insert(size_type pos, const char* s, size_type n);

Fügt bei der Position pos von *this die ersten n Zeichen aus dem C-String s, höchstens aber strlen(s) Zeichen, in den String *this ein.

string& insert(size_type pos, size_type n, char c);

Fügt n-mal das Zeichen c bei der Position pos in den durch *this kontrolli erten String ein.

string& append( const string& s); Hängt den String s an den String *this an. string& append(const char* s); Hängt den C-String s an den durch *this

gegebenen String. string& append(const string& str, size_type pos, size_type n);

Hängt n Zeichen aus dem String str, beginnend in Position pos, an den durch *this gegebenen String. Es wird höchstens bis zum letzten Zeichen von str kopiert.

Page 53: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-53

string& append(const char* s, size_type n);

Hängt die ersten n Zeichen aus dem C-String s, höchstens aber strlen(s) Zeichen, an den durch *this kontrolli erten String.

string& append(size_type n, char c);

Hängt n-mal das Zeichen c an den String *this.

string& erase(size_type pos=0, size_type n = npos);

Löscht ab der Position pos im String *this n Zeichen. Es wird höchstens bis zum letzten Zeichen von *this gelöscht. Fehlt die Angabe des zweiten Arguments, so werden ab der Position pos alle restlichen Zeichen von *this gelöscht. Wird kein Argument angegeben, so wird der String *this auf Länge 0 gekürzt.

string& replace( size_type pos, size_type n, const string& str);

Ersetzt im String *this n Zeichen ab der Position pos durch den String str. Es wird höchstens bis zum Ende von *this ersetzt.

string& replace( size_type pos1, size_type n1, const string& str,

size_type pos2, size_type n2);

Ersetzt nl Elemente ab der Position pos1 im String *this durch n2 Elemente ab der Position pos2 aus dem String str. Es wird höchstens bis zum Ende von *this bzw. str ersetzt.

string& replace(size_type pos, size_type n, const char*);

Ersetzt ab der Position pos im String *this n Elemente, höchstens aber bis zum letzten Zeichen von *this, durch den C-String s.

string& replace(size_type_pos, size_type n1,const char* s, size_type n2);

Ersetzt n1 Elemente ab der Position pos im String *this durch die ersten n2 Zeichen aus dem C-String s. Es wird höchstens bis zum Ende von *this bzw. s ersetzt.

void swap(string& str); Vertauscht den Inhalt von *this mit dem Inhalt von str. Da keine Zeichenfolgen kopiert werden, besitzt diese Methode ein sehr gutes Zeitverhalten.

string substr(size_type pos=0, size_type n=npos);

Bildet aus dem durch *this kontrolli erten String, beginnend bei der Position pos, einen Teil -String der Länge n. Es wird höchstens bis zum letzten Zeichen von *this kopiert.

Page 54: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-54

Suchmethoden size_type find(const string& str, size_type pos = 0) const;

Sucht im String *this, beginnend bei der Position pos, nach dem ersten Vorkommen des Strings str.

size_type find(const char* s, size_type pos = 0) const;

Sucht im String *this, beginnend bei der Position pos nach dem ersten Vorkommen des durch s adressierten C-Strings.

size_type find( const char* s, size_type pos, size_type n)

Sucht im String *this, beginnend bei der Position pos, nach dem ersten Vorkommen des Teilstrings, der aus den ersten n Zei-chen des char -Vektors s besteht.

size_type find(char c, size_type pos = 0) const

Sucht im String *this, beginnend bei der Position pos, nach dem ersten Vorkommen des Zeichens c.

size_type rfind(const string& str, size_type pos=0) const;

Sucht im String *this, beginnend bei der Position pos, nach dem letzten Vorkommen des Strings str.

size_type rfind(const char* s, size_type pos = 0) const;

Sucht im String *this, beginnend bei der Position pos, nach dem letzten Vorkommen des durch s adressierten C-Strings.

size_type rfind(const char* s, size_type pos, size_type n) const;

Sucht im String *this, beginnend bei der Position pos, nach dem letzten Vorkommen des Teilstrings, der aus den ersten n Zeichen des char-Vektors s besteht

size_type rfind(char c, size_type pos = 0) const;

Sucht im String *this, beginnend bei der Position pos, nach dem letzten Vorkommen des Zeichens c

Liefert bei erfolgloser Suche die Konstante size_typ string::npos zurück. Vergleichsmethoden int compare(const string&

str) const; Entspricht strcmp(this.c_str, str.c_str).

int compare(size_type pos, size_type n, const string& str) const;

Vergleicht n Zeichen des Strings *this, beginnend bei der Position pos, mit dem String str. Return-Wert: string(*this, pas, n).compare(str);

int compare(size_type pos1, size_type n1, const string& str, size_type pos2, size_type n2) const;

Vergleicht n1 Zeichen des Strings *this, beginnend bei der Position pos1, mit n2 Zeichen des String str beginnend bei der Position pos2. Return-Wert: string(*this, pos1, n1). compare(string(str,pos2,n2));

int compare( const char* s) const;

strcmp(this.c_str, s)

Page 55: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-55

Der Operator string op erator+=( PARAMETER ) liefert bei Parameterwerten von: const string& str; append(str); const char *s; append(string(s)); const char c; append(string(1, c)); Verkettungsoperatoren string operator+(const string& s1, const string& s2);

Erzeugt eine Kopie von s1 und hängt den String s2 an. Die Operator-Funktion ist wirkungsgleich mit string(s1).append(s2).

string operator+(const char* s1, const string& s2);

string(s1)+s2

string operator+(const string& s1, const char c);

str + string(1,c);

string opera tor+(char c, const string& str);

string(1,c) + str;

Returnwert ist der neue String. Ein/Ausgabe istream& operator>>(istream& is,

string& str); Liest Zeichen von der Standardeingabe bis zum Worttrenner (Voreinstellung: Whitespace) und hängt diese an den String str . Falls die Feldbreite gesetzt wurde, werden höchstens is.width() Zeichen, andernfalls str. max_size() Zeichen eingelesen. Das Einlesen wird abgebrochen wenn ein Zwischenraum- oder das Eingabeendezeichen eingegeben wird. Return-Wert: * this .

ostream& operator<<(ostream& os, string& str);

Schickt den String str zur Stardardausgabe. Return-Wert: os

istream& getline(istream &is, string &str, char delim=' \ n');

Liest eine Textzeile, die durch mit delim abgeschlossen ist, aus dem Stream is in den String str ein. delim wird nicht abgespeichert. Der alte Inhalt von str wird komplett überschrieben.

Die folgenden Vergleichsoperatoren sind für zwei Strings bzw. einem String und einem char * definiert: == != < <= > >=

Page 56: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-56

5.2 Vektoren Das in 4.3 gezeigte Beispiel für eine dynamische Vektorklasse ist (wesentlich umfangreicher und allgemeingültiger) bereits in der Standard-Template-Library implementiert: template<class Ty, class Alloc = allocator<Ty> > class vector { … }; Viele Funktionsnamen und Operationen sind bereits von den Strings bekannt. Im folgenden werden einige wichtige Operationen auf Vektoren vorgestellt: vector<int> vk(100); // Vektor 100 Integern; Defaultwert=0 class äpfel { public: äpfel(); // Default konstruktor }; ve ctor<äpfel> obst(100); // Vektor von 100 Äpfeln, jeder wird mit // dem Defaultkonstruktor initialisiert class Num { // Rechnen mit beliebiger Genauigkeit public: Num(long); // Konstruktor für Datentyp long // keinen D efaultkonstruktor! // ... }; ve ctor<Num> v1(19); // FEHLER: kein Defaultkonstruktor ve ctor<Num> v2(19, Num(0)); // Ok: Konstruktor wurde spezifiziert! Exceptions out_of_range und length_error analog zu den Strings bei Überschreiten der Vektorlänge bzw. Kapazitätsproblemen. Methoden size_type size() const; // aktuelle Anzahl von Elementen bool empty() const { return size()==0; } size_type max_size() const; // Länge des größtmöglichen Vektors void resize(size_type st, T wert=T()); // neue Werte mit wert initialis. size_type capacity() const; // Größe des belegten Speicherplatzes

// (in Elementen) void reserve(size_type n); // Platz für n Elemente schaffen -

// es erfolgt keine Initialisierung const T& operator[](size_type off) const; T& operat or[](size_type off); // Vektorzugriff (ungeprüft) auf [off] const T& at(size_type off) const; T& at(size_type off); // Vektorzugriff (geprüft) auf [off] // at geht leider auf der Thales nicht � clear(); // lösche alle Elemente insert(size_type pos, T&x); // Füge x ab Position pos ein erase(size_type pos); // lösche Element ab Position pos

Page 57: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-57

5.3 IO-Streams Aufbau der Klassenhierarchie ([Prinz98] und [Stroustrup00]):

"erbt von" bzw. "erweitert"

ios (virtuelle

Basisklasse)

istream (Einlesen aus

Streams)

ostream (Schreiben in

Streams)

ifstream

(Lesen aus Dateien)

ofstream (Schreiben in

Dateien)

iostream (u.a.Eingabe/

Ausgabe Terminal)

fstream (Erweiterung bzgl.

Dateien)

Page 58: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-58

Die Klassen haben die folgenden Aufgaben [Aupperle97]: ios include <iostream> Basisklasse, die zuständig für die Führung

von Formatierungsdaten (z.B. Anzahl Nach-kommastellen) und der Streamzustände (z.B. Lese-/Schreibfehler) ist. ios enthält hauptsächlich diese Statusflags und Funktionen zur Abfrage der Flags.

istream include <iostream> Objekte der Klasse istream übernehmen allgemeine Eingabeaufgaben. Die Klasse enthält eine Reihe von vordefinierten <<-Operatoren für die gängigen Datentypen. Sie erbt als abgeleitete Klasse die Informationen der Basisklasse ios, um z.B. die Art der Eingabe zu bestimmen. Treten Fehler auf, werden die Zustandsbits der Basisklasse ios gesetzt.

ostream include <iostream> Analog zu istream - aber zuständig für die Ausgabe.

ifstream include <fstream> Erbt die Eigenschaften von istream, kann aber auch aus Dateien lesen.

ofstream include <fstream> Erbt die Eigenschaften von ostream, kann aber auch in Dateien schreiben.

iostream include <iostream> Erbt die Eigenschaften von istream und ostream. Objekte der Klasse können daher sowohl aus Streams lesen wie auch in Streams schreiben.

fstream include <fstream> Lesen von und Schreiben in Dateien. Erbt Eigenschaften von iostream.

Page 59: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-59

5.3.1 Ein einleitendes Beispielprogramm // einfaches "copy" Kommando: // copy file1 file2 kopiert file1 in file2 // copy file1 zeigt file1 am Bildschirm an // Zeiger auf Basistyp erlaubt Verweise auf den abgeleiteten Typ (AI 2) #include <fstream> #include <iostream> #include <stdio.h> #include <string> int main(int argc, char **argv) { if (argc<2 || argc >3) cerr << "usage: "<< argv[0] <<": file1 [file2]"<<endl, exit(1); ifstream in(argv[1]); // Eingabedatei oeffnen if (!in) perror(argv[1]), exit(1); ofstream file; // eigentliche Dateiverbindung ostream *out; // Zeiger auf Basistyp geht auch bei // abgeleiteten Typen // oder man nimmt (void *) ;-) der geht immer if (argc==3) { // Ausgabedatei file.open(argv[2], ios::out); // Datei zum Schreiben oeffnen // Attribute ios::[in, out, app, trunc, ate, binary] if (!file) { perror(argv[2]), exit(1); } out = &file; // Datei als Ausgabestrom merken } else out = &cout; // nehme Standardausgabe while (!in.eof()) { string line; getline(in, line); // dynamisches Lesen bis zum Newline *out <<line<<endl; // klappt, da ofstream die Eigenschaften // von ostream erbt } in.close(); if (out != &cout) // cout *nicht* schliessen ((ofstream*)out)->close(); // nur ofstream hat ein .close() return 0; }

Page 60: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-60

5.3.2 Standardstreams und -operatoren Die folgenden Standardstreamobjekte stehen immer zur Verfügung: cin class istream Standardeingabe liest von Tastatur cout class ostream Standardausgabe schreibt auf

Bildschirm cerr class ostream Standardfehlerausgabe

(ungepuffert) schreibt auf Bildschirm

clog class ostream Standardfehlerausgabe (gepuffert)

schreibt auf Bildschirm

Der Linksshiftoperator << steht für Ausgabe und der Rechtsshiftoperator >> für Eingabeopera-tionen bereit. Beide Operatoren sind für die Basisdatentypen wie int, long, char* etc. bereits definiert und können auch für eigene Klassen nachträglich implementiert werden (siehe z.B. 4.1.10): ostream& operator<<( ostream &out, const neue_klasse &neu ); istream& operator>>( istream &in, const neue_klasse &neu);

5.3.3 Fehlerzustände [Josuttis94] Die Zustände der Ein/Ausgabestreams werden in der Basisklasse ios festgehalten. Es existieren eine Reihe von Abfragefunktionen, die die jeweili gen Zustände als Boolean zurückliefern: good() true, wenn alles in Ordnung ist (ios::goodbit gesetzt) eof() true bei End-Of-File (ios::eofbit) fail() true bei Fehler (ios::failbit oder ios::badbit) bad() true bei fatalem Fehler (ios::badbit) rdstate() liefert Kombination der gesetzten Flags zurück clear() löscht (kein Parameter) und setzt Flags (mit Parameter) operator void*()

liefert true zurück, wenn der stream in Ordnung ist (if (cin) { /* */ })

operator!() liefert true zurück, wenn der stream nicht in Ordnung ist (if (!cin) { /* */ })

Page 61: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-61

5.3.4 Standardelementfunktionen zur Eingabe Neben den Operatoren << und >> existieren eine Reihe von Elementfunktionen zum Einlesen in der Klasse istream im Zusammenhang mit char bzw. char*: int get() liefert das nächste gelesene Zeichen oder die

Integer-Konstante EOF bei Eingabeende. ist ream& get(char& ch) Weist das nächste Zeichen an den übergebenen

Parameter ch zu und liefert den Stream zurück. Der Zustand des Streams gibt jeweils Auskunft über den Erfolg der Operation.

istream& get(char* s, int anz, char ende=' \ n')

Liest bis zu anz - 1 Zeichen in den String s ein. Beim Zeichen ende (default = Zeilenende) wird das Einlesen beendet. ende wird nicht mit abgespeichert. Ein 0Byte beendet den char[] .

int gcount() Anzahl der zuletzt via get() gelesenen Zeichen. istream& read(char *p, int anz ) liest maximal n Zeichen in p ein. istream& ignore(int n=1, int ende =EOF)

Überliest n Zeichen - bzw. maximal bis zum EOF.

int peek() liefert das nächste Zeichen, ohne es einzulesen istream& putback(char ch) stellt das Zeichen in den Eingabestrom zurück,

damit es erneut gelesen werden kann. Achtung: bei allen Funktionen werden Trennzeichen (Whitespace) nicht überlesen.

5.3.5 Standardelementfunktionen zur Ausgabe ostream& put(char& ch) gibt das Zeichen ch aus ostream& write(char *s, int anz) gibt anz Zeichen des Strings s aus ostream& flush() leert den Ausgabepuffer

5.3.6 Verknüpfung von Ein- und Ausgabe ostream *tie(ostream* stream); verknüpft einen Stream (z.B. istream cin ), um sicher zu stellen, dass vor einer Einleseoperation über den Stream, der anderer (Ausgabe-)Stream vollständig geleert ist. Es gilt immer: cin.tie(&cout); dadurch klappt sicher die folgende Operation: string s; cout << "Passwort: "; cin >>s; Wenn cin und cout nicht verbunden wären, ist nicht gewährleistet, dass man die Ausgabe "Passwort" sieht, bevor man via cin das Passwort einlesen kann. Ansonsten müßte man explizit angeben: cout <<"Passwort: "; cout.flush(); cin >>s;

Page 62: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-62

5.3.7 Kleines Kopierprogramm mit Statusabfrage #include <iostream> void main() { char c; while (cin.get(c)) cout .put(c); if (!cin.eof()) cerr <<"Lesefehler" <<endl; if (!cout.good()) cerr <<"Schreibfehler" <<endl; }

5.3.8 Manipulatoren flush ostream Ausgabepuffer leeren endl ostream '\n' ausgeben ends ostream '\0' ausgeben ws istream Whitespace überlesen Der Aufruf cout <<endl; entspricht endl(cout);

5.3.9 Formatdefinitionen Die Einstellungen für die Ein/Ausgabeformate werden als Flags (Bitfeld) in ios abgespeichert und können über die Funktion flags() ausgelesen werden: typedef unsigned long fmtflags; fmtflags f lags() const { return _flags; } cout <<cout.flags()<<endl; Mit Hil fe der Funktion setf() und unsetf() können einzelne Bits gezielt gesetzt und gelöscht werden: cout.setf(ios::showpos | ios::uppercase); // Vorzeichen, Großbuchstaben cout.unsetf(ios::upper case); // Klein/Großschreibung wieder beachten // klappt leider unter gcc 2.95 nicht � Die Funktion width() setzt die Feldbreite bei der Ein- und Ausgabe und fill(char ch) definiert das Füllsymbol (Voreinstellung: Blank). cout.unsetf(ios::dec); cout.setf(ios::hex); schaltet die Ausgabe von Dezimal auf Hexadezimal um. Hier ist auch ios::oct für Oktaldarstellung der Zahlen möglich. Diese Manipulatoren sind auch direkt verwendbar: cout << 314 <<" "<< hex << 314 << oct <<" "<<314<<endl;

Page 63: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-63

Durch setzen von cout.setf(ios::showbase); werden die Zahlen bei der Ausgabe mit Ihrem Zahlensystem gekennzeichnet (Ausgabe wäre dann: "314 0x13a 0472"). precision() setzt die Genauigkeit von Gleitkommazahlen (Anzahl Stellen nach dem Dezimal-punkt) bei der Ausgabe. Mit setprecision() kann während der Ausgabe umgeschaltet werden: #include <iomanip.h> cout << 3.1415926535 <<" "<< setprecision(3) << 3.1415 <<endl; Um einen alten Ausgabezustand wiederherzustellen, empfiehlt sich das folgende Vorgehen: fmtflags oldflags = cout.flags(); // Zustand merken cout.setf(ios::showpos |ios::uppercase); // Ausgabeformat einstellen cout <<1 <<" enten schwimmen auf dem See"<<endl; cout.flags(oldflags); // Urzustand wiederherstellen

5.3.10 Dateizugriff Streamklassen können auch für den Zugriff auf Dateien verwendet werden: ifstream (input file stream), ofstream (output file stream) und fstream (file stream). Die Headerdatei für alle drei Klassen ist #include <fstream> Beim Anlegen von Objekten der Klassen werden die zugehörigen Dateien automatisch geöffnet (Angabe des Dateinamens über den Konstrukore: istream in("eingabe")) und beim Zerstören der Objekte automatische wieder geschlossen. Schlägt das Öffnen fehl, so kann das durch den "!" Operator getestet werden: if (!in) ... Soll eine Datei nicht beim Zerstören eines Objekts geschlossen werden, so muss mit Pointern gearbeitet werden: ifstream *in = new ifstream("eingabe"); Die folgenden Dateiflags stehen beim Öffnen zur Verfügung und können via Bit-Oder verknüpft werden: ios::in Lesen (default bei ifstream) ios::out Schreiben (default bei ofstream) ios::app Anhängen ios::ate ans Ende positionieren ("at end") ios::trunc alten Dateiinhalt löschen ios::nocreate Datei muß bereits existieren ios::noreplace Datei darf noch nicht existieren (nicht über-

schreiben!) ios::binary Datei binär öffnen - keine Konvertierung von

Newline in Carriage Return + Newline (Windos)

Page 64: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-64

In den folgenden zwei Zeilen wird eine Datei zum Schreiben geöffnet. Sie muß bereits existieren, ansonsten schlägt das Öffnen fehl. Der alte Inhalt wird gelöscht und "aber hallo" hinein geschrieben: ofstream fp("file", ios::trunc | ios::nocreate); if (fp) fp <<"aber hallo"<<endl; Zum freien Positionieren in Dateien existieren die folgenden Elementfunktionen: istream& seekg(long pos) setzt die absolute Leseposition ostream& seekp(long pos) setzt die Schreibposition long tellg() und long tellp() liefern Lese- und Schreibposition Ein Aufruf der Funktionen bei cin und cout ist nicht definiert. Den seekX() - Funktionen können als weiterer Parameter die relative Positionsangabe mitgegeben werden: ios::beg relativ vom Dateianfang ios::cur relativ zur aktuellen Position (Voreinstellung) ios::end relativ zum Dateiende Mit einem negativen Parameter pos kann man sich "rückwärts" positionieren: datei.seekg( - 5, ios::cur); // 5 Zeichen vor akt. Pos. zurück Man kann aus einem Filedeskriptor nachträglich einen Stream machen - ("Systemnahe Software"!): int fd = 1; // Filedeskriptor Nr. 1 = Standardausgabe ofstream stdout(f d); stdout <<"Mal sehen, wo das ausgegeben wird ..."<<endl;

Page 65: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-65

5.4 Die String-Stream-Klasse - Nearly everything is a stream. Die Mechanismen der Stream-Klassen können verwendet werden, um aus Strings zu lesen und in Strings zu schreiben (s.a. Vorlesung Allgemeine Informatik II - Streamklasse in Oberon). So ist es z.B. oft sinnvoll , erst eine komplette Zeile vom Terminal in einen String einzulesen, um sie dann sukzessive in einzelne Teilelemente zu zerlegen. Die folgende Abbildung gibt einen Überblick über die passende Klassenhierarchie (#include <sstream>) [Stroustrup00]:

"erbt von" bzw. "erweitert"

ios (virtuelle

Basisklasse)

istream (Einlesen aus

Streams)

ostream (Schreiben in

Streams)

istringstream

(Lesen aus Strings)

ostringstream (Schreiben in

Strings)

iostream

stringstream

fstream

Page 66: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 5-66

thales$ cat strstream1.cc // Lesen aus einem Stringstream #include <sstream> #include <iostream> main() { string line; // normaler String getline(cin, line); // normales Einlesen von stdin istringstream lstr(line);// erzeuge Stream, initial. mit String // kopiere Ergebnis nach lstr.str() int x; lstr >>x; // verzehrendes Einlesen aus Stream string rest; lstr >>rest; // lese restlichen String ein cout <<"x="<<x<<" rest="<<rest<<endl; } thales$ a.out 12:gibmirdenrest x=12 rest=:gibmirdenrest thales$ cat strstream2.cc // Schreiben in einen Stringstream #include <sstream> #include <stdio.h> #include <iostream> main() { ostringstream lstr; // erzeuge Stringstream für Ausgabe int x = 123; lstr <<"x="<<x<<endl; // Schreiben lstr.str() cout <<lstr.str(); // Ausgabe des erzeugte n Strings } thales$ a.out x=123

Page 67: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-67

6 Vererbung Ein wesentliches Ziel der OOP ist es, eine gute Wiederverwendbarkeit bereits vorhandener Komponenten (Klassen, Bibliotheken) zu erreichen und zu unterstützen (z.B. durch Templates). Wir betrachten im folgenden zwei Arten der Wiederverwendung bestehender Klassen: Die Part-of-Beziehung ("ein Objekt einer Klasse ist Teil einer anderen Klasse") und die Is-a-Beziehung ("eine Klasse erbt alle Elemente und Eigenschaften einer Basisklasse"). Die Is-a-Beziehung kann durch die Part-Of-Beziehung "nachgebaut" werden (sog. Delegation).

6.1 Objekte als Klassenelemente Beispiel für Part-of-Beziehung zwischen den Klassen Vorlesung, Student und Datum: Eine Vorlesung wird von einer Menge von Studenten gehört. Ein Student besitzt (u.a.) ein Geburtsdatum: class Datum { int tag, monat, jahr; public: /* ... */ }; class Student { Datum gebdat; string name, vorname; long matrikelnr; public: /* ... */ }; class Vorlesung { vector<Student> teilnehmer; string dozent; // .... public: }; Die Part-of-Beziehung kann also ohne weiteres mit den bisher gesehenen C++-Sprachmitteln ausgedrückt werden.

6.2 Vererbung Beispiel für eine IS-A-Beziehung zwischen Person, Student, Wima-Student: Ein Wima-Student ist ein spezieller Student (er/sie studiert Wirtschaftsmathe, braucht zum Bestehen des Studiums spezielle Scheine etc.). Der Student ist im wesentlichen eine natürlich Person, die als weiteres Merkmal eine Matrikelnummer besitzt (Sichtweise der Verwaltung -):

Page 68: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-68

thales$ cat erbe.cc // Einfaches Beispiel fuer Vererbung: ein Student ist eine Person // ein Wimastudent ist ein Student und damit auch eine Person :-) (nett!) #include <string> // Person ist die Basisklasse class person { string name, vorname; // alle Personen haben einen Namen int alter; // und sind irgendwann geboren worden public: // Defaultkonstruktor person(string vn="vn", string n="nach", int alt=0): name(n), vorname(vn), alter(alt) {} // Lesezugriff auf den Namen string getname() const { return vorname+" "+name; } // Lesezugriff auf das Alter int getalter() const { return alter; } }; // Ein Student ist eine Person :-) deshalb "erbt" die Klasse student alle // Eigenschaften und Methoden der Klasse Person // bzw. "Die Klasse Student ist eine Erweiterung der Klasse Person" class student : public person { string matrikelnr; // Zusatz fuer Student: Matrikelnummer public: student(string vn="vn",string n="nach",int alt=0,string mn="(leer)"): /* init. des Personenteils: */ person(vn, n, alt), matrikelnr(mn) {} string getname() const // Alles auslesen { // Funktion getname in Person aufrufen return person::getname() + " matrikel="+matrikelnr; } }; // ein Wimastudent ist ein spezieller Student // die Klasse erbt alles von der Klasse student class wimastudent : public student { bool sysoftschein; // Wimas muessen Softscheine machen! public: wimastudent(string vn="vn", string n="nach", int alt=0, string mn="(leer)", bool soft=false): student(vn, n, alt, mn), sysoftschein(soft) {} bool hatersoft() const { return sysoftschein; } }; main() { person peter("heinz-peter", "urmisch", 35); cout <<peter.getname()<<endl; student franz("franz", "urmisch", 35,"1235566"); cout <<franz.getname()<<endl; // Funktion student::getname() cout <<franz.person::getname()<<endl; // Funktion person::getname() wimastudent josch("josef", "urmisch", 21,"1566"); cout <<josch.getname() <<": hat "; // student::getname if (!josch.hatersoft()) cout <<"k"; cout <<"einen Schein in Systemnahe Software!"<<endl; }

Page 69: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-69

Person ist die Basisklasse für Student. Die Klasse Student erbt dadurch alle Elemente und Elementfunktionen der Klasse Person. Die ursprünglichen Sichtbarkeitsmerkmale nach außen (private und public) werden bei der Public-Vererbung ("class student : public

person ...) eins zu eins an die Klasse Student weitergereicht: name und vorname sind z.B. für Funktionen außerhalb der Klasse Person nicht zugänglich - damit auch nicht für Elementfunktionen der Klasse Student. Wäre das der Fall , würde das Konzept des Schutzes von Private-Variablen durch die Vererbung sehr einfach ausgehebelt werden können -. Funktionen der Basisklasse können in der abgeleiteten Klasse überdefiniert werden (student::getname()), sind aber bei Angabe des Namensbereichs trotzdem gezielt aufrufbar (franz.person::getname()). Der Aufruf einer Elementfunktion einer abgeleiteten Klasse für Objekte der Basisklasse ist natürlich nicht sinnvoll: person peter("heinz-peter", "urmisch", 35); cout <<peter.student::getname() <<endl; // Compiler gibt Fehlermeldung! Der Konstruktor von Student muss die Elemente der Basisklasse Person initialisieren, kann sie aber natürlich nicht direkt ansprechen, da die Elemente private sind. Deshalb gibt es hier die Möglichkeit der Angabe des sog. Basisinitialisierers für die Klasse Person, der bei der Definition des Konstruktors hinter dem ':' angegeben wird (analog zum Elementinitialisierer - siehe unter 9.4): student(...): person(vn, n, alt), ... { } Die Klasse Wimastudent erweitert die Klasse Student und erbt damit (implizit) auch die Elemente und Elementfunktionen der Klasse Person. Der "Aufbau" bzw. Initialisierung der Objekte erfolgt stets von innen nach außen. Das heißt im konkreten Fall wird bei der Erzeugung eines Objektes der Klasse Wimastudent, zuerst der Person-Teil i nitialisiert, dann die Student-Elemente und zum Schluß das zusätzliche Element sysoftschein in Wimastudent: Person->Student->Wimastudent Der Abbau erfolgt dann beim Destruktor in entgegengesetzter Reihenfolge: Wimastudent->Student->Person Bemerkung: Oft heißt die Basisklasse auch Superklasse oder Oberklasse und die abgeleitete Klasse dann Unterklasse. Diese Begriffe sind aber etwas irreführend, da die Unterklasse meist eine Obermenge der Elemente und Funktionen der Oberklasse besitzt -. Im folgenden werden die Begriffe Basisklasse und abgeleitete Klasse weiter verwendet.

Page 70: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-70

6.3 Arten der Vererbung Neben den Schlüsselwörtern private und public gibt es noch eine dritte Art von Klassenelementen, die eigentlich nur im Zusammenhang mit Vererbung Sinn macht: protected Elemente. Via protected deklarierte Elemente einer Klasse sind vor direktem Zugriff ausserhalb von Elementfunktionen geschützt, analog zu private Elementen. Bei einer public Vererbung kann aber die abgeleitete Klasse B auf die Elemente der Basisklasse A zugreifen: class A { //private: string topsecret; // ausserhalb nicht sichtbar protected: string alittlebitsecret; // sichtbar in abgel. Klasse // bei public Vererbung public: string opentotheworld; // für alle Welt sichtbar void give_a_talk() { cout<<topsecret+" "+alittlebitsecret+" "+opentotheworld<<endl; } }; class B : public A { // Public - Vererbung erlaubt Zugriff für die public: // Klasse B auf protected Elemente von A

// … void show_your_content() { cout <<topsecret<<endl; // FEHLER! private bleibt pri vate cout <<alittlebitsecret<<endl;// erlaubt! Erbe darf zugreifen! cout <<opentotheworld<<endl; // public bleibt public … } }; Neben der Publicvererbung gibt es noch weitere Arten der Vererbung. Die folgende Tabelle gibt eine Übersicht über die Zugriffsmöglichkeiten von außerhalb auf die Elemente der abgeleiteten Klasse B, die bereits in der Klasse A enthalten waren: Art der Vererbung in A: public in A: private in A: protected class B : public A {…} wird in B public wird in B private wird in B protected class B : private A {…} wird in B private wird in B private wird in B private class B : protected A {…} wird in B protected wird in B private wird in B protected (Bsp: int c sei public in A; Vererbung: class B: private A{}; b sei ein Objekt der Klasse B; dann kann auf b.c außerhalb von B nicht zugegriffen werden, da es nach außen private ist!)

6.4 Implizite Typumwandlungen und Zuweisungen Bei der Vererbung übernimmt die abgeleitete Klasse alle Eigenschaften und Fähigkeiten der Basisklasse. Deshalb sind Objekte der abgeleiteten Klasse in gewisser Weise spezielle Objekte der Basisklasse. Darum können Anweisungen (Operationen, Zuweisungen) an ein Objekt der

Page 71: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-71

Basisklasse genauso gut an die Objekte der abgeleiteten Klasse erfolgen. Hierbei passiert eine implizite Typumwandlung in den Typ der Basisklasse: class p erson { string name, vorname; int alter; public: // ... }; class student : public person { string matrikelnr; public: //… }; void main() { person oliver("oliver","gruenspan", 42); student karl("karl","mursel", 22, "1231512"); person *p = &karl; // Zeiger vom Basisklassentyp ok person k = karl; // Zuweisung an Basisklassenobjekt ok student s = oliver; // ungültig! wie soll die MatrNr lauten? student *sp = &oliver; // ungültig! } Bei einer Zuweisung von Student-Objekten an Person-Objekte werden sämtliche Komponenten kopiert, die in Person und Student gemeinsam enthalten sind ("Slicing" oder "Projektion"). Eine umgekehrte Zuweisung student s = oliver; kann nicht erfolgen, da dann z.B. nicht definiert wäre, welche Matrikelnummer der Student s erhalten soll , da oliver keine Matrikelnummer besitzt. Der Pointer person *p = &karl; kann auf alle öffentliche Methoden und Elemente der Basisklasse zugreifen. Neue Element der Klasse Student (z.B. die Matrikelnummer) bleiben dem Zugriff entzogen. Ein Zugriff kann aber durch einen Cast erzwungen werden: ((student*)p) - >matrikelnr = "124141213"; // ok!

6.5 Polymorphie Zeiger auf Basisklassen von verschiedenen verwandten Objekten werden z.B. dann eingesetzt, wenn eine gemeinsame Verwaltung der verwandten Objekten geführt werden soll (Container, Listen). In einer einer zentralen "Studentenverwaltung" soll z.B. eine Liste von Zeigern auf Objekten aus unterschiedlichen Studentenklassen (Wimastudent, Biostudent etc.) geführt werden. Würde diese Liste nicht mit Zeigern arbeiten, könnten Objekte unterschiedlicher Klassen nicht gemeinsam verwaltet werden, da bei einer echten Zuweisung immer eine Projektion auf die zugrundeliegende Klasse passieren würde und damit Informationen verloren gingen:

Page 72: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-72

class student { // … }; c lass wimastudent : public student { // … }; class biostudent : public student { // ... }; main() { vector<student> liste(20);

wimastudent w; biostudent b; liste[0] = w; liste[1] = b; // Projektion bei Zuweisung vernichtet Infos! // ...

} Die richtige Lösung wäre hier ein Vektor von Zeigern des Basistyps: main() { vector<student*> liste(20);

wimastudent *w = new wimastudent; biostudent *b = new biostudent; liste[0] = w; liste[1] = b; // Zeiger erhalten alle Infos // ...

} Jetzt steckt man aber in einem anderen Dilemma: welche Funktion soll aufgerufen werden, wenn sämtliche Klassen beispielsweise eine eigene Methode printInfo() implementieren? thales$ cat poly1.cc // Container fuer Studenten - Dilemma der statischen Bindungen #include <iostream> #inc lude <vector> #include <string> class student { // Basisklasse string matNr; public: void printInfo() { cout <<"I am a student"<<endl; } }; class wimastudent : public student { // Wimastudent // ... new stuff for wima public: void printInfo() { cout < <"I am a wimastudent"<<endl; } }; class biostudent : public student { // Biostudent // ... new stuff for bio public: void printInfo() { cout <<"Sorry, I am a bio student"<<endl; } };

Page 73: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-73

void main() { vector<student*> hoersaal; // da sind verschiedene studis drin hoersaal.resize(hoersaal.size()+1, new student); hoersaal.resize(hoersaal.size()+1, new wimastudent); hoersaal.resize(hoersaal.size()+1, new biostudent); for (int i=0; i<hoersaal.size(); i++) { hoersaal[i]->printInfo(); // aber welches printInfo()?? } } thales$ a.out I am a student I am a student I am a student Da beim Erzeugen des ausführbaren Programms keine weiteren Informationen über printInfo() vorhanden sind, verläßt der Compiler sich auf die printInfo()-Methode des Basistyps (statische Bindung zur Compili erzeit). Einzig bekannter Ausweg hieraus wäre bis jetzt die Erzeugung einer weiteren Klasse, die sich neben dem Zeiger auf die Basisklasse Student auch noch den eigentlichen Typ des Objekts merkt und dann beim Aufruf von printInfo() über eine grosse switch()-Anweisung jeweils die richtige Methode aufruft. Das bedeutet aber, dass diese Switch-Anweisung für jede neue Klasse einen neuen Case-Fall erhalten muss.

6.5.1 Das Schlüsselwort virtual Ein Ausweg, der sprachintern wegen der gerade gezeigten Problematik geschaffen werden mußte, geht den Weg über sog. virtuelle Funktionen: class student { // Basisklasse string matNr; public: virtual void printInfo() { cout <<"I am a student"<<endl; } };

Dadurch wird der Compiler angewiesen, die Funktion printInfo() beim Erzeugen des Codes nicht statisch einzubinden, sondern ein sog. late binding zu ermöglichen. Es wird dann zur Laufzeit bei einem Zeiger auf einen Basistyp automatisch die richtige Methode der abgeleiteten Klasse gesucht und - falls sie implementiert wurde - aufgerufen. Mit obiger Basisklasse student ergibt sich dann die korrekte Ausgabe: thales$ a.out I am a student I am a wimastudent Sorry, I am a bio student Intern werden virtuelle Funktionen durch Zeiger auf Funktionen realisiert, die zur Laufzeit noch "umgehängt" werden können.

Page 74: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-74

6.5.2 Der Begriff "Polymorphismus" (sinngemäß nach [Stroustrup00]) "Dass man 'das richtige Verhalten' für eine Funktion der Klasse Student unabhängig davon erhält, welche Art von Student man eigentlich benutzt, wird Polymorphismus genannt. Eine Klasse mit virtuellen Funktionen wird polymorphe Klasse genannt. Um ein polymorphes Verhalten in C++ zu erzielen, müssen die aufgerufenen Elementfunktionen virtuell sein, und die Objekte müssen über Zeiger oder Referenzen manipuliert werden. Wenn man ein Objekt direkt bearbeitet, ist sein Typ dem Compiler bekannt, und es wird kein Polymorphismus zur Laufzeit benötigt." (bzw. ermöglicht!)

6.5.3 Eigenschaften virtueller Funktionen Eine in der Basisklasse als virtual gekennzeichnete Funktion ist auch in allen abgeleiteten Klassen virtuell . Diese Eigenschaft wird quasi mitvererbt. Umgekehrt wird eine Methode in einer Basisklasse nicht rückwirkend dadurch virtuell , dass sie in einer abgeleiteten Klasse als virtuell redefiniert wird. Eine virtuelle Methode einer Basisklasse muss nicht in der abgeleiteten Klasse redefiniert werden. Sie erbt dann einfach (wie gehabt) die Methode der Basisklasse. Bei einer Redefinition einer virtuelle Methode in der abgeleiteten Klasse ist zu beachten, dass die Redefinition die identische Signatur (definiert durch die Parameterliste!) wie die Methode in der Basisklasse besitzt. Eine Redefinition einer Methode über eine unterschiedliche Signatur ist natürlich nicht mehr virtuell - es wird beim Aufruf eine Methode mit der passenden Signatur gesucht (und im unteren Fall i n der Basisklasse gefunden!): class biostudent : public student { // Biostudent // ... new stuff for bio public:

int printInfo(char *s) { cout <<"Sorry, I am a bio student"<<endl; return 1;}

}; .... thales$ a.out I am a student I am a wimastudent I am a student

6.5.4 Virtuelle Destruktoren Eine Klasse, die als Basisklasse für andere Klassen dient, sollte immer einen virtuellen Destruktor besitzen. Ansonsten würde beim delete-Aufruf eines Pointers auf den Basistyp immer nur der Destruktor der Basisklasse aufgerufen:

Page 75: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-75

thales$ cat poly2.cc // Notwendigkeit von virtuellen Destruktoren #include <iostream> class student { // Basisklasse public:

~student() // Destruktoren sollten virtual sein!! { cout <<"zerstoere Typ Student"<<endl;}

}; class wimastudent : public student { // Wimastudent public: ~wimastudent() { cout <<"zerstoere Typ Wimastudent"<<endl;} }; void main() { student *s = new wimastudent; delete s; } thales$ a.out zerstoere Typ Student Mit der Definition virtual ~student() { /*...*/} ergibt sich die folgende Ausgabe: thales$ a.out zerstoere Typ Wimastudent zerstoere Typ Student

6.5.5 Abstrakte Klassen In einigen Fällen findet man eine gemeinsame Basisklasse für mehrere Klassen, diese entspricht aber u.U. keinem "realen" Gegenstück. So wäre eine gemeinsame Basisklasse für Rechteck, Kreis und Quadrat die geometrische Form. Eine "geometrische Form an sich" gibt es aber nicht. Dennoch kann man sagen, dass geometrische Formen allgemeine Eigenschaften (z.B. Flächeninhalt) und Operationen besitzen (Drehen und Anzeigen der Form), die natürlich der Basisklasse zugeordnet werden. Für eine Basisklasse Form macht eine Implementierung der Anzeige aber z.B. keinen Sinn, da noch keine konkrete Form vorliegt. Will man solche abstrakten Klassen festlegen, so definiert man in dieser Klasse mindestens eine rein virtuelle Funktion. Die entsprechende Syntax lautet: class geoForm { // Beispiel einer abstrakten Klasse // ... // Bsp. einer rein virtuellen Fkt.

virtual void zeigeform() = 0; // es exist. keine Implementierung! }; Objekte von abstrakten Klassen können nicht instanziiert werden (geoForm f; geht nicht!), da nicht alle Methoden definiert (implementiert) wurden. Abgeleitete Klassen sollten dann diese rein virtuellen Funktionen definieren. Wenn sie es nicht tun, bleiben sie wiederum abstrakte Klassen, was u.U. gewünscht sein kann. Abstrakte Klassen stellen ein allgemeines und polymorphes Interface für abgeleitete Klassen dar. Die abgeleiteten Klassen sollten möglichst das Interface nicht mehr erweitern, sondern nur mehr implementieren.

Page 76: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-76

Hier das komplette Beispiel für die Klasse geometrische Form: thales$ cat form.cc // Beispiel fuer eine abstrakte Klasse: geometrische Form #include <iostream>

// geom. Form "an sich" gibt es nicht = abstrakte Klasse class geoForm { // Interface für alle konkreten Formen (Kreis, Quadrat) float inhalt; // Flaecheninhalt der Form (falls berechenbar) public: geoForm(float f=0) : inhalt(f) { } float flaecheninhalt() { return inhalt; } virtual void zeigeform() = 0; // ex. keine Implementierung! virtual void dreheform(int grad) = 0; // ex. keine Implementierung! virtual ~geoForm() { } // Destruktor sollte virtuell sein!! }; class kreis : public geoForm { // ein Kreis ist eine geom. Form int mittelx; int mittely; // Mittelpunkt float radius; // Radius public: kreis(int mx, int my, float radius) : mittelx(mx), mittely(my), geoForm(radius*radius*3.1415) { } void zeigeform() { cout <<"O"<< endl;} // to be programed later ... void dreheform(int grad) { } // to be programed later ... ~kreis() { } // nothing to do ... }; class quadrat : public geoForm { // ein Quadrat ist eine Form int linksobenx, linksobeny; // linke obere Ecke float seitenlaenge; public: quadrat(int lx, int ly, int len) : linksobenx(lx), linksobeny(ly), geoForm(len*len) { } void zeigeform() { cout <<"[]"<< endl;} // to be done later ... void dreheform(int grad) {} // to be programed later ... ~quadrat() { } // nothing to do ... }; void main() { //geoForm g; // geht nicht! abstrakte Klasse! geoForm *f; // geht! macht allein aber keinen Sinn! f = new quadrat(2,3, 10); // Quadrat der Seitenlaenge 10 geoForm *k = new kreis(3,3, 1); // Kreis mit Radius 1 um (3, 3) cout << "ich bin so gross: "<<f->flaecheninhalt(); cout <<" und sehe so aus: "; f->zeigeform(); cout << "ich bin so gross: "<<k->flaecheninhalt(); cout <<" und sehe so aus: "; k->zeigeform(); } thales$ a.out ich bin so gross: 100 und sehe so aus: [] ich bin so gross: 3.1415 und sehe so aus: O

Page 77: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-77

6.6 Dynamische Casts und Objekt-Ids Objektzeiger vom Typ der Basisklasse können auch auf abgeleitete Klassen zeigen, wie wir bereits gesehen haben. Ist die Basisklasse polymorph, dann findet das Programm auch zur Laufzeit korrekt die Methoden der passenden Klasse für einen Funktionsaufruf. Manchmal ist es aber auch erwünscht, direkt den Typ der abgeleiteten Klasse wieder zu ermitteln, auf den der Basisklassenzeiger verweist. Hierzu gibt es den dynamischen Cast und den Operator Typeid. Der dynamische Cast funktioniert nur auf Zeiger vom Typ polymorpher Basisklassen - ansonsten gibt es eine Fehlermeldung während des Compili ervorgangs. Es wird überprüft, ob ein Cast auf eine zweite angegebene Klasse zur Laufzeit möglich ist. Der Cast findet statt, wenn die gewünschte Klasse in der Klassenhierachie unterhalb (oder gleich) der angegebenen Klasse ist. Rückgabewert ist ein Zeiger vom gewünschten Typ im Erfolgsfall , ansonsten ein Nullzeiger: thales$ cat dyncast.cc // kleines Beispiel zum dynamischen Cast // kann zur Laufzeit (polymorphe) Klassenhierarchien erkennen #include <iostream> #include <string> class A { public: virtual ~A() {} // mache aus A eine polymorphe Klasse }; class B : public A { }; class C : public B { }; class D { }; main() { A *a = new C(); // Ok, da A Großvater von C ist B *b = dynamic_cast<B*> (a); // Ok, da B Vater von C ist D *d = dynamic_cast<D*> (a); // A und D sind nicht verwandt B *hlp = new B(); C *c = dynamic_cast<C*> (hlp); // Nein: C ist abgeleitet von B if (b) cout <<"DynCast fuer b hat geklappt"<<endl; else cout <<"DynCast fuer b hat nicht geklappt"<<endl; if (d) cout <<"DynCast fuer d hat geklappt"<<endl; else cout <<"DynCast fuer d hat nich t geklappt"<<endl; if (c) cout <<"DynCast fuer c hat geklappt"<<endl; else cout <<"DynCast fuer c hat nicht geklappt"<<endl; } thales$ a.out DynCast fuer b hat geklappt DynCast fuer d hat nicht geklappt DynCast fuer c hat nicht geklappt

Während der dynamische Cast indirekte Informationen über den vorliegenden Typ gibt, liefert der Operator typeid u.a. direkt den Namen der Klasse, auf den der Pointer zeigt. Auch hier muss die

Page 78: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-78

Basisklasse wieder virtuell sein. Es gibt sonst zwar keine Fehlermeldung beim Compili eren, aber der Typid-Operator liefert dann nicht die gewünschte Information. thales$ cat typid.cc // kleines Beispiel zur Funktion typeid #include <typeinfo> #include <iostream> #include <string> class A { public: virtual ~A() {} // mache aus A eine polymorphe Klasse }; class B { }; class C : public A { }; class D : public A { }; main() { cout <<"A has id: "<<typeid(A()).name()<<endl; cout <<"B has id: "<<typeid(B()).name()<<endl; cout <<"new B has id: "<<typeid(new B()).name()<<endl; cout <<"new A has id: "<<typeid(new A()).name()<<endl; cout <<"C has id: "<<typeid(C()).name()<<endl; cout <<"D has id: "<<typeid(D()).name()<<endl; A *ptr = new A(); cout <<"A ptr to A has id: "<<typeid(ptr).name()<<endl; cout <<"*A ptr to A has id: "<<typeid(*ptr).name()<<endl; // wahrer Typ wird nur erkannt, wenn A polymorph ist! ptr = new C(); cout <<"*A ptr to C has id: "<<typeid(*ptr).name()<<endl; ptr = new D(); cout <<"*A ptr to D has id: "<<typeid(*ptr).name()<<endl; void *p = new C(); cout <<"void * ptr to C has id: "<<typeid(*p).name()<<endl; } thales$ a.out A has id: Fv_1A B has id: Fv_1B new B has id: P1B new A has id: P1A C has id: Fv_1C D has id: Fv_1D A ptr to A has id: P1A *A ptr to A has id: 1A *A ptr to C has id: 1C *A ptr to D has id: 1D void * ptr to C has id: v

Page 79: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-79

6.7 Mehrfachvererbung class boot { int ps; enum bootsklasse; public:

void einsteigen(); void anlassen();

void losfahren(); void sos_senden(); void heraus_schauen(); }; class haus { int zimmer; float wohnflaeche; public: void haustuer_oeffnen(); void heizen(); void fernseher_anschalten(); void heraus_schauen(); }; class hausboot : public haus, public boot { }; main() { hausboot hb; hb.haustuer_oeffnen(); hb.einsteigen(); hb.heizen(); hb.anlassen(); hb.losfah ren(); hb.fernseher_anschalten(); // ... hb.boot::heraus_schauen(); // Namenskonflikt auflösen } Eine Klasse kann von mehreren Basisklassen erben. Wie bei der Einfachvererbung, erbt sie dann sämtliche Methoden und Elemente aller Basisklassen. Namenskonflikte müssen (so wie auch bei der Einfachvererbung) über die Angabte von Namensbereichen gelöst werden: hb.boot::heraus_schauen(); Die Mehrfachvererbung kann natürlich auch als eine Kombination einer HAS-A und einer IS-A-Beziehung nachgebaut werden. class hausboot : public boot {

haus boot; }; main() { hausboot hb; hb.boot.haustuer_oeffnen(); hb.einsteigen(); hb.boot.heizen(); hb.anlassen(); hb.losfahren();

hb.boot.fernseher_anschalten(); // ... hb.heraus_schauen(); // kein Namenskonflikt mehr d a! } C++ ist eine der wenigen Programmiersprachen, die Mehrfachvererbung (direkt) unterstützen.

Page 80: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-80

6.8 Virtuelle Basisklassen Bei der Mehrfachvererbung kann es vorkommen, dass eine Basisklasse über Umwege mehrmals an eine (indirekt) abgeleitete Klasse weitervererbt wird:

In diesem Fall i st es eigentlich nicht erwünscht, dass die Elemente der Klasse Flugzeug mehrfach in Objekten der Klasse Truppentransporter erscheinen - was aber leider der Fall i st: thales$ cat vclass.cc // Notwendigk eit von virtuellen Klassen #include <iostream> #include <string> class fzeug { // ein Flugzeug public: string id; fzeug(string s="(fzeug)") { id = s; } }; class passfzeug : public fzeug { // ein Passagierflugzeug public: passfzeug(string i nit="(passfzeug)") : fzeug(init) {} }; class milflieger : public fzeug {// ein Militärflugzeug public: milflieger(string init="(milflieger)") : fzeug(init) {} }; class truppenflieger : public milflieger, public passfzeug {}; main() { truppenflieger jet; cout << jet.milflieger::id<<endl; cout << jet.passfzeug::id<<endl; } thales$ a.out (milflieger) (passfzeug)

Flugzeug

Milit är-maschine

Truppen-transporter

Passagier-flugzeug

Flugzeug

Page 81: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 6-81

Um diese Doppelvererbung zu vermeiden, wird das Konzept der virtuellen Klassen eingeführt: class milflieger : public virtual fzeug { //… class passfzeug : public virtual fzeug { //… Dadurch wird die gewünschte Vererbungsstruktur erreicht:

Beim Ausführen des Programmes ergibt sich jetzt: thales$ a.out (fzeug) (fzeug) Hier wird 2 x "(fzeug) " ausgegeben - obwohl die Initialisierer der Klassen milflieger und passfzeug versuchen, die Klasse fzeug mit "ihren" Werten zu initialisieren. Das liegt daran, dass bei virtuellen Klassen der Konstruktor genau einmal aufgerufen wird, und zwar in der von der Basisklasse am weitetesten entfernten Klasse - und das ist hier truppenflieger . Ansonsten kann es bei Mehrfachaufrufen des Konstruktors für ein und dasselbe Objekt (der Basisklasse fzeug ) zu erheblichen Problemen (Speicherverwaltung, eigene Objektverwaltung!) kommen. Begriffswiederholung: polymorphe Klasse besitzt mindestens eine virtuelle Funktion

(virtual void f() { }) taucht auf im Zusammenhang mit dynamischer Bindung, Pointern

abstrakte Klasse besitzt mindestens eine rein virtuelle Funktion (virtual void f () =0 ); kann nicht instanziiert werden!

virtuelle Klasse Klasse, die über das Schlüsselwort virtual vererbt wird; spielt nur bei der Mehrfachvererbung eine Rolle

Flugzeug

Milit är-maschine

Truppen-transporter

Passagier-flugzeug

Page 82: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-82

7 Einblick in die Standard Template Library (STL) von C++ "Entscheiden Sie, welche Algorithmen Sie benötigen. Parametrisieren Sie sie so, dass sie für eine Vielzahl von geeigneten Typen und Datenstrukturen arbeiten" [Stroustrup00] Die Template-Konstruktion in C++ ist kein OO-Konzept. Die Kernidee besteht in der sprachlichen Unterstützung der Trennung von Algorithmen und Datenstrukturen ("generisches Programmieren"). Die in C++ enthaltene Standard Template Library kann grob in drei Bestandsmerkmale unterteilt werden: Container (Verwalter einer Menge von Objekten) Iteratoren (Zeiger auf Objekte in Containern; können den Container "durchwandern") Algorithmen, Operationen auf dem Container (sortieren, suchen, löschen etc.)

7.1 Containertemplates Eine Klasse, deren Hauptaufgabe es ist, Objekte zu verwalten, wird Containerklasse genannt. C++ stellt die folgenden Standardcontainer zur Verfügung: #include Beschreibung Beispiel <vector> Vektor/Array; kann dynamisch

wachsen; einfacher Zugriff über Index Speicherung von Meßergebnissen bei äquidistanten Zeitintervallen

<list> doppelt verkettete lineare Liste (rasches Einfügen/Löschen möglich)

Meßergebnisse mit unterschiedlichen Zeitintervallen; ggf. Löschen nötig

<queue> Warteschlange: First-In-First-Out (Fifo) angelsächsische Warteschlange: man stellt sich schön brav hinten an und nur der vorderste wird bedient

<deque> Double ended queue: Einfügen und Löschen ist an beiden Enden möglich

bundesrepublikanische Warte-schlange (manche drängeln vorne rein, andere scheren hinten verbittert aus -)

priority_queue in <queue>

Warteschlange, bei der die Abarbeitung der Elemente wichtigkeitsgesteuert ist

Vorziehen von FirstClass- und Business-Passagieren beim Einsteigen ins Flugzeug

<stack> First-In-Last-Out (Stapel) eingleisiger Sackbahnhof <map> "assoziatives Array": ungeordnetes

Array, bei dem der Zugriffsindex aus einem beliebigen Datentyp bestehen kann (z.B. string)

Studentenverwaltung über eindeutige Matrikelnummer

multimap in <map>

wie map; jeder Schlüssel kann mit mehreren Werten assoziiert sein

Schlüssel: Matrikelnummer Werte: Telefonnummern des Studenten

<set> ungeordnete Menge (ein Element/ Objekt darf nur einmal enthalten sein)

Studenten in einem Hörsaal

multiset in <set>

Elemente können mehrfach auftauchen Lottozahlen beim Ziehen mit Zurücklegen

Page 83: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-83

7.2 Iteratoren: Zugriff auf Elemente beliebiger Container Iteratoren sind Zeiger auf Objekte in einem Container. Mit Iteratoren kann man durch die Liste der Objekte eines Containers wandern ("iterieren"). Iteratoren stellen für alle STL-Container ein einheitli ches Interface zur Verfügung. Wie normale Zeiger unterstützen Iteratoren Operatoren wie * und ++, um der Reihe nach Elemente ansprechen zu können. Da sie über die Datenstrukturen des Containers Kenntnis besitzen müssen um diese Aufgabe zu erfüllen, findet man sie jeweils in den jeweili gen Containerklassen definiert. Jeder Container muß seinen Datentyp als eigenes Element iterator bereitstellen, wobei es sich entweder um eine eigenständige (Verwalter-)Klasse oder um einen einfachen lokale typedef handelt. Eine Implementierung von vector könnte z.B. die folgenden Bestandteile haben: template <class T> class vector { public: vector(); // ... typedef T* iterator; typedef const T* const_iterator; typedef T valuetype; // ... private: T *data; };

const _it erator sollte für einen lesenden Zugriff auf Objekte Verwendung finden. valuetype stellt den variablen Typ des Templates als typedef zur Verfügung.

Ein Iterator läßt sich mittels operator++ von dem Wert, der durch die Elementfunktion begin() des Containers geliefert wird bis zum Wert von end() fortschalten. Der von end() gelieferte Wert ist dabei nicht der letzte gültige für den Iterator, sondern der erste ungültige. Im folgenden Beispiel werden alle Elemente eines Vektors ausgegeben. main() { vector< double> v(20); vector<double>::iterator i; // typedef innerhalb der vector Klasse s.o. // v[] füllen … for ( i=v.begin(); i != v.end(); ++i ){ cout << *i << endl; } } Die STL unterscheidet die folgenden wesentlichen Iteratortypen:

• random access iterator ("wahlfreier Zugriff"): unterstützt die übliche Zeigerarithmetik (Manipulation durch -- , ++, die Addition oder Subtraktion ganzer Zahlen und Indizierung durch [] ). Zugriffe auf das Element selber erfolgen mit * oder ->. Vergleiche der Iteratoren sind möglich mit ==, <, >, != . Anwendbar auf vector, string, deque .

• forward iterator: beschränkt auf das elementweise Voranschreiten im Container. Es wird nur ++ als arithmetische Operation, Elementzugriff mit * und - > sowie Vergleiche auf Gleichheit und Ungleichheit unterstützt.

Page 84: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-84

• backward iterator: Rückwärtsiteratoren unterstützen sinngemäß das Navigieren mit -- und ebenfalls Vergleiche auf Gleichheit und Ungleichheit

• bidirectional iterators sind Iteratoren, die sowohl die Eigenschaften von Vorwärts- als auch die von Rückwärtsiteratoren aufweisen. (Einsatz bei list, set, multiset, map, multimap) .

Container können (soweit sinnvoll ) in umgekehrter Reihenfolge durchlaufen werden: vector <double> v(20); // ... for (vector<double>::reverse_iterator i=v.rbegin(); i!=v.rend(); i++)

//… ;

Elementzugriff : top() , front() und back() Kopie erstes bzw. letztes Element Stack, List und Queue: push(), push _back() am Ende anfügen pop(), pop _back() letztes Element entfernen push _front() neues erstes Element anfügen (list + deque ) pop_front() erstes Element entfernen (list + deque ) allgemeine Operationen: insert(p, x) einfügen von x vor p insert(p, n, x) fügt n Kopien von x vor p ein erase(p) lösche Element bei p clear() lösche alle Elemente size() Anzahl Elemente empty() ist Container leer? max_size() max. Anzahl von Elementen im Container resize() Größe verändern (vector, deque, list ) swap() zwei Elemente vertauschen stack: push(), pop(), top() queue: pus h(), pop(), front(), back() deque: push_front(), push_back(), pop_front(), pop_back(), front(), back(), [] list: push_front(), push_back(), pop_front(), pop_back(), front(), back()

7.3 Operationen und Algorithmen auf den Elementen der Container Die STL stellt 60 Operationen und Algorithmen zur Verfügung, mit denen der Inhalt der Container durchsucht, sortiert, durchwandert etc. werden kann. Die Funktionen stehen nach einem #include <algorithm> zur Verfügung. Einzelheiten - wie z.B. die Parameterliste der Funktionen - findet man u.a. auf http://www.sgi.com/tech/stl ). Die folgenden Tabellen sind [Stroustrup00] entnommen. Eine Sequenz wird durch 2 Iteratoren innerhalb eines Containers begrenzt: vector<int> v(20 ); // … vector<int>::iterator fnd42 = find(v.begin(), v.end(), 42);

Page 85: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-85

7.3.1 Einige einführende Beispielprogramme thales$ cat vek42.cc //Vektorzugriff durch [] und Iteratoren #include <vector> #include <iostream> #include <algorithm> typedef vector<int> int vec; void main() { intvec v(10); fill(v.begin(), v.end(), 42); for (int i=0; i<v.size(); i++) cout <<v[i]<<" "; cout <<endl; for (intvec::iterator i = v.begin(); i!=v.end(); i++) cout <<*i<<" "; } thales$ a.out 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 thales$ cat list42.cc //Iteratorenbeispiel für Listen #include <list> #include <iostream> #include <algorithm> typedef list<int> intlist; void main() { intl ist v(10); fill(v.begin(), v.end(), 42); for (intlist::iterator i = v.begin(); i!=v.end(); i++) cout <<*i<<" "; } thales$ a.out 42 42 42 42 42 42 42 42 42 42 thales$ cat rev.cc //Elementfolge einer Sequenz umkehren via reve rse() #include <algorithm> #include <string> #include <iostream> void main() { string a = " :falO ollaH"; string b = "ein neger mit gazelle zagt im regen nie"; reverse(a.begin(), a.end()); reverse(b.begin(), b.end()); cout <<a<<b<<endl; } thales$ a.out Hallo Olaf: ein neger mi tgaz ellezag tim regen nie

Page 86: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-86

thales$ cat merge.cc // zwei sortierte Sequenzen zusammenfuegen #include <algorithm> #include <vector> #include <list> #include <iostream> void main() { vector<int> ger; vector<int> unger; for (int i=0; i<20; i+=2) { ger.resize(ger.size()+1); ger[i/2] = i; } for (int i=1; i<20; i+=2) { unger.resize(unger.size()+1); unger[(i-1)/2] = i; } // Vektoren ger und unger zusammenfuegen; Resultat in Liste res list<int> res(ger.size()+unger.size()); fill(res.begin(), res.end(), 42); merge(ger.begin(), ger.end(),

unger.begin(), unger.end(), res.begin()); for (list<int>::iterator i = res.begin(); i!=res.end(); i++) cout <<*i <<" "; cout <<endl; } thales$ a.out 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 thales$ cat stack.cc //simpelstes Stackbeispiel #include <algorithm> #include <stack> #include <iostream> void main() { stack<int> st; for (int i=0; i<10; i++) st.push(i); while (!st.empty()) cout <<st.top()<<" ", // oberstes Element holen st.pop(); // ... jetzt loeschen cout <<endl; } thales$ a.out 9 8 7 6 5 4 3 2 1 0

Page 87: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-87

thales$ cat accu.cc // Aufsummieren der Zahlen einer Liste #include <algorithm> #include <list> #include <iostream> #include <numeric> void main() { list<int> q; for (int i=0; i<10; i++) q.push_front(i); cout <<accumulate(q.begin(), q.end(), 0) <<endl; } thales$ a.out 45 thales$ cat skalar.cc // Inneres Produkt von {1, 1, 3} und {1, 3, 1} #include <algorithm> #include <list> #include <iostream> #include <numeric> void main() { list<int> a; list<int> b; a.push_front(1); a.push_front(1); a.push_front(3); b.push_front(1); b.push_front(3); b.push_front(1); cout <<inner_product(a.begin(), a.end(), b.begin(), 0) <<endl; } thales$ a.out 7 thales$ cat q.cc // komplexes Queue-Beispiel: hinten rein, vorne raus #include <algorithm> #include <queue> #include <iostream> void main() { queue<int> q; for (int i=0; i<10; i++) q.push(i); while (!q.empty()) cout <<q.front()<<" ", // oberstes Element holen q.pop(); // ... und aus Queue entfernen cout <<endl; }

Page 88: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-88

thales$ a.out 0 1 2 3 4 5 6 7 8 9 thales$ cat set1.cc // Teilmengentest #include <set> #include <iostream> #include <algorithm> typedef set<int> intset; void main() { int a[] = { 1, 3, 5, 7, 9}; int b[] = { 3, 5, 7}; intset aset(&a[0], &a[4]); // Mengen a und b intset bset(&b[0], &b[2]); // Initialisierung ueber einen Vektor cout <<"Die Menge A enthaelt die Menge B"; if (includes(aset.begin(), aset.end(), bset.begin(), bset.end())) cout <<"."; else cout <<" nicht."; cout <<endl; } thales$ a.out Die Menge A enthaelt die Menge B.

7.3.2 Übersicht: Nicht-modifizierende Sequenzoperationen for_each() für jedes Element eine Operation durchführen find() erstes Auftreten eines Wertes finden find_if() erstes Element, das ein Prädikat erfüllt , finden find_first_of() beliebigen Wert einer Sequenz in einer anderen Sequenz finden adjacent_find() zwei identische benachbarte Werte finden count() Anzahl des Vorkommens eines Wertes zählen count_if() Anzahl der Elemente, die ein Prädikat erfüllen, zählen mismatch() erste unterschiedliche Elemente zweier Sequenzen finden equal() testen, ob zwei Sequenzen die gleichen Elemente haben search() erstes Auftreten einer Teilsequenz finden find_end() letztes Auftreten einer Teilsequenz finden search_n() erste Teilsequenz mit n gleichen Werten finden

Page 89: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-89

7.3.3 Übersicht: Modifizierende Sequenzoperationen transform() auf jedem Element eine Operation durchführen copy() alle Elemente, beginnend mit dem ersten, kopieren copy_backwards() alle Elemente, beginnend mit dem letzten, kopieren swap() zwei Elemente vertauschen iter_swap() zwei Elemente, auf die Iteratoren zeigen, vertauschen swap_ranges() alle Elemente zweier Sequenzen vertauschen replace() Elemente mit einem bestimmten Wert ersetzen replace_if() Elemente, die ein Prädikat erfüllen, ersetzen replace_copy() Elemente kopieren und dabei einen bestimmten Wert ersetzen replace_copy_if() Elemente kopieren und diejenigen, die ein Prädikat erfüllen, ersetzen fill() alle Elemente durch einen Wert ersetzen fill_n() n Elemente durch einen Wert ersetzen generate() alle Elemente durch das Ergebnis einer Operation ersetzen generate_n() n Elemente durch das Ergebnis einer Operation ersetzen remove() Elemente mit einem bestimmten Wert entfernen remove_if() Elemente, die ein Prädikat erfüllen, entfernen remove_copy() Elemente kopieren und dabei einen bestimmten Wert entfernen remove_copy_if() Elemente kopieren und diejenigen, die ein Prädikat erfüllen, entfernen unique() aufeinanderfolgende Duplikate entfernen unique_copy() Elemente kopieren und dabei aufeinanderfolgende Duplikate entfernen reverse() die Reihenfolge der Elemente umkehren reverse_copy() Elemente kopieren und dabei die Reihenfolge der Elemente umkehren rotate() Elemente rotieren rotate_copy() Elemente kopieren und dabei rotieren random_shuffle() Elemente mischen

7.3.4 Übersicht: Sequenzen sortieren sort() sortieren (mit guter durchschnittli cher Eff izienz) stable_sort() sortieren (gleiche Elemente behalten ihre Reihenfolge) partial_sort() einen Teil einer Sequenz sortieren, dabei aber alle Elemente

berücksichtigen: {4,3,5,1,9}[1..3]->{1,3,4,9,4,5} partial_sort_copy() Elemente kopieren und dabei den ersten Teil sortieren nth_element() das n.te Element an die richtige Stelle sortieren (links davon

ist alles kleiner, rechts davon alles größer; ->Quicksort) lower_bound() die erste Position eines neuen Elements in einer sortierten

Sequenz finden, die die Sortierung nicht verletzt upper_bound() die letzte Position eines neuen Elements in einer sortierten

Sequenz finden, die die Sortierung nicht verletzt equal_range() eine Teilsequenz mit einem bestimmten Wert finden binary_search() Ist ein Wert in einer sortierten Sequenz enthalten? merge() zwei sortierte Sequenzen vereinigen inplace_merge() zwei hintereinanderliegende sortierte Sequenzen vereinigen

Page 90: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-90

partition() Elemente, die ein Prädikat erfüllen, nach vorn plazzieren; die anderen Elemente kommen nach hinten

stable_partition() Elemente, die ein Prädikat erfüllen, unter Beibehaltung der relativen Reihenfolge nach vorn platzieren

7.3.5 Übersicht: Mengenalgorithmen includes() Ist eine Sequenz in einer anderen enthalten? set_union() erzeugt eine sortierte Vereinigungsmenge set_intersection() erzeugt eine sortierte Schnittmenge set_difference() erzeugt eine sortierte Menge der Elemente, die in einer

Sequenz und in einer zweiten nicht vorhanden sind

set_symmetric_difference() erzeugt eine sortierte Menge der Elemente, die in genau einer von zwei Sequenzen vorhanden sind

Für die meisten Mengenoperationen sind sog. Outputiteratoren notwendig, die hier aber nicht weiter behandelt werden können.

7.3.6 Übersicht: Minimum und Maximum min() den kleineren von 2 Werten liefern max() den größeren von 2 Werten liefern min_element() das kleinste Element einer Sequenz liefern max_element() das größte Element einer Sequenz liefern lexicographical_compare() zwei Sequenzen lexikographisch vergleichen (größer/kleiner)

7.3.7 Übersicht: Numerische Algorithmen Via #include <numeric> bietet die STL einige verallgemeinerte numerische Algorithmen an: accumulate() Verknüpfung von Operationen auf einer Sequenz (Standard:

Addition) {1, 3, 8, 1} -> 13 inner_product() Verknüpfung von Operationen auf zwei Sequenzen (Standard:

Skalarprodukt) {1, 1, 2}, {1, 5, 1} -> 8 partial_sum() Sequenz aus Operationen auf einer Sequenz durchführen

(Standard: Partialsumme) {1,2,3,4,5} -> {1,3,6,10,15} adjacent_difference() Sequenz aus Operationen auf einer Sequenz durchführen

(Standard: Subtraktion) {1, 2, 4, 7} -> {1, 1, 2, 3 } Die auf den Sequenzen standardmäßg angewandten Operatoren können ersetzt werden.

Page 91: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-91

7.3.8 Funktionsobjekte (Funktoren) Viele Algorithmen bearbeiten die Sequenzen unter Verwendung von Iteratoren und (konstanten) Werten:

fill(v.begin(), v.end(), 42); Oft ist es aber notwendig, während des Durchlaufs durch eine Sequenz die Informationen der einzelnen Objekte aufzusammeln. Dazu können sog. Funktionsobjekte (oder Funktoren) für die Operation zur Verfügung gestellt werden: thales$ cat sum.cc #include <list> #include <iostream> #include <algorithm> typedef list<int> intlist; template <class T> class Summe { // Funktorenklasse T res; public: Summe(T i=0) : res(i) {} // Konstruktor void operator()(T x) { res += x; } // eigentliche Auswertung T ergebnis() const { return res; } }; void main() { intlist v(10); fill(v.begin(), v.end(), 42); Summe<int> s; s = for_each(v.begin(), v.end(), s); // Elemente aufsummieren cout <<s.ergebnis() <<endl; } thales$ a.out 420

Die Funktorenklasse muss einen Operator bereitstellen, der auf den Parameter der Klasse angewandt werden kann: template <class T> class Summe { public: operator(T x) {} };

Page 92: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 7-92

for_each() testet nicht, ob im 3.Parameter ein Zeiger auf eine Funktion steht, oder ein Objekt. Das folgende Programm liefert das gleiche Ergebnis: int summe; // globale Variable void sum(int i) { summe += i; } main() { // ... v wie oben for_each(v.begin(), v.end(), sum); // Zeiger auf Funktion cout <<summe <<endl; } Es wird einfach versucht einfach, den 3.Parameter auf jedes Objekt des Containers anzuwenden. Die Ursprungzeile aus sum.cc ist äquivalent zu: for (intlist::iterator i=v.begin(); i!=v.end(); i++) s(*i); Ein Beispiel zum Sortieren (klappt nicht für die list-Klasse; es muss ein random_access_iterator definiert sein!): thales$ cat sort.cc #include <vector> #include <iostream> #include <algorithm> typedef vector<int> intvec; bool lessthan(int a, int b) { return a<b; } void main() { intvec v(10); int j = 1000; for (intvec::iterator i=v.begin(); i!=v.end(); i++) { j--; *i = j; // Zahlenfolge absteigend } for (intvec::iterator i=v.begin(); i!=v.end(); i++) cout <<*i <<" "; cout <<endl; sort(v.begin(), v.end(), lessthan); // aufsteigend sortieren for (intvec::iterator i=v.begin(); i!=v.end(); i++) cout <<*i <<" "; cout <<endl; } thales$ a.out 999 998 997 996 995 994 993 992 991 990 990 991 992 993 994 995 996 997 998 999

Page 93: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 8-93

8 Übersicht über die UML Die Unified Modeling Language (UML) ist eine Sprache zur Spezifikation, Visualisierung, Konstruktion und Dokumentation von Modellen für Softwaresysteme und Nicht-Softwaresysteme (z.B. Geschäftsmodelle). Sie bietet den Entwicklern die Möglichkeit, den Entwurf und die Entwicklung von Modellen auf einheitli cher Basis - unabhängig von konkreten Programmier-sprachen - zu diskutieren. Die UML wird seit 1998 als standardisiert (aktuell: Version 1.3) angesehen. Sie besteht im wesentlichen aus:

• Klassendiagrammen (class diagram) o Basiselemente

��Klassen, Objekte ... o Beziehungselemente

��z.B. Vererbung • Verhaltensdiagrammen

o Aktivitätsdiagramm (activity diagram) ��Ablaufverfolgung von Objekten

o Kollaborationsdiagramm (collaboration diagram) ��zeitli ches, ereignisbezogenes Zusammenspiel von Objekten

o Sequenzdiagramm (sequence diagram) ��ähnlich wie Kollaborationsdiagramm; Schwerpunkt auf zeitli chen Verlauf

des Nachrichtenaustauschs zwischen Objekten o Zustandsdiagramm (state diagram)

��Startzustände, Endzustände und Zustandsübergänge bei Objekten; ein Zustand ist hier als Attributkombination der Klassensattribute zu verstehen

• Implementierungsdiagrammen o Komponentendiagramm (component diagram)

��Zusammenspiel einzelner getrennter Software-Komponenten (z.B. Schnitt-stellen zwischen Komponenten; Zusammenspiel Client-Server)

o Einsatzdiagramm (deployment diagram) ��ggf. Ablauf verschiedener Komponenten auf verschiedenen Rechnern (sog.

"Knoten") (Bsp.: Client-Server) • Anwendungsfalldiagramm (use case diagram)

��Modelli erung des Zusammenspiel von verschiedenen Akteuren, die die Software benutzen, und Anwendungsfällen

Page 94: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 8-94

8.1 Klassendiagramme

8.1.1 Basisdokumentation und Vererbung Hier das leicht erweiterte Programm der abstrakten Klasse geoForm aus 6.5.5 (Erweiterungen in Fettdruck): thales$ cat form.cc // Beispiel fuer eine abstrakte Klasse: geometrische Form #include <iostream>

// geom. Form "an sich" gibt es nicht = abstrakte Klasse class geoForm { // Interface für alle konkreten Formen (Kreis, Quadrat) float inhalt; // Flaecheninhalt der Form (falls berechenbar) static int objcnt; // Gesamtanzahl erzeugter geoForm-Objekte public: geoForm(float f=0) : inhalt(f) { objcnt++; } float flaecheninhalt () { return inhalt; } virtual void zeigeform() = 0; // ex. keine Implementierung! virtual void dreheform(int grad) = 0; // ex. keine Implementierung! virtual ~geoForm() { } // Destruktor sollte virtuell sein!! static int numObjects }; static int geoForm::objcnt = 0; int geoForm::numObjects() { return objcnt; } class kreis : public geoForm { // ein Kreis ist eine geom. Form int mittelx; int mittely; // Mittelpunkt float radius; // Radius public: kreis(int mx, int my, float radius) : mittelx (mx), mittely(my), geoForm(radius*radius*3.1415) { } void zeigeform() { cout <<"O"<< endl;} // to be programed later ... void dreheform(int grad) { } // to be programed later ... ~kreis() { } // nothing to do ... }; class quadrat : public geoForm { // ein Quadrat ist eine Form int linksobenx, linksobeny; // linke obere Ecke float seitenlaenge; public: quadrat(int lx, int ly, int len) : linksobenx(lx), linksobeny(ly), geoForm(len*len) { } void zeigeform() { cout <<"[]"<< endl;} // to be done lat er ... void dreheform(int grad) {} // to be programed later ... ~quadrat() { } // nothing to do ... }; main() { // ... }

Page 95: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 8-95

Nachfolgend eine mögliche Darstellung der Klassen in UML 1.3 samt Erklärung der einzelnen Elemente:

Ein Interessantes Problem ergibt sich bei der Erweiterung der Klassenhierarchie:

Spezialisierung

Quadrat

- linksobenx:int { linksobenx >=0} - linksobeny:int { linksobeny >=0} - seitenlaenge:float { >0} - inhalt:float {inhalt=seitenlaenge²} zeigeform():void dreheform(grad:int=0):void ~quadrat()

Kreis

- mittelx:int {mittelx>=0} - mittely:int {mittely>=0} - radius:float {radius>0} - inhalt:float ��� ������ ��� ����� ������� � � zeigeform():void dreheform(grad:int=0):void ~kreis()

Opera-tionen

Klassen- attribute

GeoForm { abstrakt }

- inhalt:float = 0 {inhalt >= 0} - objcnt:int = 0 {objcnt >=0} flaecheninhalt():float zeigeform():void { abstrakt } dreheform(grad:int=0):void { abstrakt } ~geoForm() { virtuell } numObjects():int

public

Klassen- operation

Zusicherung

abstrakte Klasse Klassen- variable

private

"erweitert"

Klassen- name

GeoForm {abstrakt}

Ellipse

Kreis {F1 = F2}

Rechteck

Quadrat {a=b}

Spezialfall: identische

Brennpunkte

Spezialfall: identische

Seitenlängen

Page 96: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 8-96

Bei dieser Lösung ist zu beachten, dass Quadrat eine Spezialisierung der Rechtecks-Klasse ist, aber keine echte Erweiterung. Es wird lediglich die Zusicherung { a = b } hinzugefügt. Betrachtet man die Klassenhierarchie unter dem Stichwort Erweiterung, dann ist auch die folgende Beziehung denkbar:

Bei dieser Lösung wird die Spezialisierung zugunsten der Erweiterung (weniger Speicherbedarf!) geopfert. Daraus entsteht dann z.B. ein (semantisches) Problem bei der Zuweisung: rechteck r; quadrat q = r; Die Zuweisung ist syntaktisch richtig (Slicing!) und "stanzt" aus einem Rechteck ein Quadrat aus, was bezüglich der Semantik schwer zu vermitteln ist. Bei dem vorherigen Modell würde bei Zuweisung eines Quadrats an ein Rechteck die Eigenschaften des Quadrats erhalten bleiben.

8.1.2 Assoziationen Die Klasse FigurenSammlung enthalte beliebig viele (auch keine) geometrische Formen:

Er wei ter ung

GeoForm {abstrakt}

Kreis

Ellipse

Quadrat

Rechteck

Erweiterung: zweiter

Mittelpunkt ...

Erweiterung: zweite

Seitenlänge

enthält

0..* 1

FigurenSammlung formen:set<GeoForm> anzeigen() ...

GeoForm {abstrakt}

... ....

Quadrat

... ....

Kreis

... ....

Page 97: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 8-97

Page 98: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-98

9 Anhang

9.1 Bubblesort als Funktionstemplate Ausgehend von dem kleinen C-Programm bsort.c erstellen wir ein Sortier-Template: thales$ cat bsort.c /* * Bubblesort fuer Integer */ void bubblesort(int arr[], int len) { int sortiert = 0; // flag fuer alles sortiert j/n while (!sortiert) { int i; sortiert = 1; for (i=1; i< len; i++) { if (arr[i-1]> arr[i]) { int tmp = arr[i-1]; arr[i-1] = arr[i]; arr[i] = tmp; sortiert = 0; } } --len; } } #define max 8 int main(void) { int zahlen[max] = { 3, 1, 77, 4, 9, 12, 13, 23 }; int i; bubblesort(zahlen, max); for (i=0; i<max; i++) printf("%d\n", zahlen[i]); }

Das Template sieht dann wie folgt aus: thales$ cat sort.h /* * Bubblesort via Templates */ // Defaultsvergleichsfunktion fuer den Vergleich von Zahlen template <class T> int numcmp(T a, T b) { if (a>b) return 1;

Page 99: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-99

else if (a==b) return 0; else return - 1; } // tauschen von 2 Elementen template <class T> void swap(T &a, T &b) { T c = a; a = b; b = c; } // Bubblesort - Template // arr = Array von zu sortierenden Elementen // len = Laenge des Arrays // cmp = Vergleichsfunktion für 2 Elemente - liefert >0, 0, <0 // Voreinstellung: numerischer Vergleich der Elemente (numcmp) template <class T> void bubblesort( T arr[], int len, int (*cmp)(T a, T b)=numcmp) { int sortiert = 0; // flag fuer alles sortiert j/n while (!sortiert) { sortiert = 1; for (int i=1; i< len; i++) { if (cmp(arr[i - 1], arr[i])>0) { swap(arr[i - 1], arr[i]); sortiert = 0; } } -- len; } }

Das Template kann man nun z.B. mit 2 Testprogrammen prüfen: thales$ cat main1.cc // Beispielprogramm 1 fuer den Gebrauch des Sort - Templates // aufwaerts Sortieren von Integer #include "sort.h" #include <stdio.h> const int max = 8; int main() { int zahlen[max] = { 3, 1, 77, 4, 9, 12, 13, 23 }; bubblesort(zahlen, max); for (int i = 0; i<max; i++) printf("%d \ n", zahlen[i]); return 0; } thales$ cat main2.cc

Page 100: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-100

// Beispielprogramm 2 fuer den Gebrauch des Sort - Templates // auf und abwaer ts sortieren von Strings #include "sort.h" #include <stdio.h> int aufwaerts(char *a, char *b) {// leider ist strcmp: int strcmp(const void char* a, const void char *b) // deshalb muss hier der Umweg über die Funktion mycmp gegangen werden return strcmp( a, b); } int abwaerts(char *a, char *b) { return strcmp(b, a); } int main() { char *str[] = { "der", "Loewe", "Hans", "steht", "ganz", "allein", "auf", "einem", "Stein", "auf", "einem", "Bein" }; int len = sizeof(str)/sizeof(*str); bubblesort(s tr, len, aufwaerts); for (int i = 0; i<len; i++) puts(str[i]); getchar(); bubblesort(str, len, abwaerts); for (int i = 0; i<len; i++) puts(str[i]); return 0; }

9.2 Lebenszyklus von Objekten thales$ cat life.cc // Veranschaulichung der Lebenszykle n von Objekten #include <iostream> int objid = 0; // eindeutige Objektid / Anzahl erzeugter Obj. class student { char *name; long matrikelnr; int id; public: student(char *init="", long m=0l) { name = new char[strlen(init)+1]; strcpy(name, init) ; matrikelnr = m; id = ++objid ; // Nummer des erzeugten Objekts cout <<"erzeuge Student: "<<name << "(" << matrikelnr << ")" ;

Page 101: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-101

cout << " (Objekt Nr. "<<id<<")"<<endl; } ~student() { cout << "loesche Student: "<<name<< "(" << matrikelnr << ")" ; cout << " (Objekt Nr. "<<id<<")"<<endl; delete [] name; } student(student &); // Der Kopierkonstruktor student &operator=(student &); // Der Zuweisungsoperator void showyourname() const // Methode aendert Obj NICHT! { cout << "Hallo ich bin "<<name<<" ("<<matrikelnr<<")"<<endl; cout << "(Das sagte Objekt Nr. "<<id<<")"<<endl; } }; student::student(student &a) // Kopierkonstruktor { name = new char[strlen(a.name)+1]; strcpy(name, a.name); matrikelnr = a.matrikelnr; cout << "Kopierkonstruktor called for "<<name<<" id="<<a.id<<endl; id = ++objid; cout <<

"Kopierkonstruktor: Habe Obj Nr "<<id<<" initialisiert!"<<endl; } student &student::operator=(student &a) // Zuweisungsoperator { name = new char[strlen(a.name)+1]; strcpy(name, a.name); matrikelnr = a.matrikelnr; cout << "Zuweisungsoperator called for "<<name<<" id="<<a.id<<endl; return *this; } void fkt(student person) // ersetze Kopie der Var durch Referenz: &person { cout <<"** in fkt - vor showyourname"<<endl; person.showyourname(); cout << "** Ende von fkt"<<endl; } int main() { student karl("karl", 523312); student hans("hans", 52312); cout <<"** fkt(hans)"<<endl; fkt(hans); cout <<"** vor hans = karl"<<endl; hans = karl; cout <<endl<<

"Es wurden "<<objid<<" Objekte insgesamt erzeugt."<<endl; cout <<

"So: Jetzt wird main beendet. Wurde auch Zeit!"<<endl<<endl;; } thales$ a.out erzeuge Student: karl(523312) (Objekt Nr. 1)

Page 102: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-102

erzeuge Student: hans(52312) (Objekt Nr. 2) ** fkt(hans) Kopierkonstruktor called for hans id=2 Kopierkonstruktor: Habe Obj Nr 3 initialisiert! ** in fkt - vor showyourname Hallo ich bin hans (52312) (Das sagte Objekt Nr. 3) ** Ende von fkt loesche Student: hans(52312) (Objekt Nr. 3) ** vor hans = karl Zuweisungsoperator called for karl id=1 Es wurden 3 Objekte insgesamt erzeugt. So: Jetzt wird main beendet. Wurde auch Zeit! loesche Student: karl(523312) (Objekt Nr. 2) loesche Student: karl(523312) (Objekt Nr. 1)

9.3 Achtung bei Operatorenfunktionen mit einer Referenz auf temporäre Objekte thales$ cat opera.cc // kleines Testbeispiel fuer die Operatorenueberlagerung // operator=(obj&) geht *nicht* bei temporaeren Objekten!!!! #include <iostream> #include <stdio.h> const int maxlen=20; class obj { char name[maxlen]; // Name des Objekts public: obj(char *s="") { // Konstruktor cout <<"create "<<s<<endl; strcpy(name, s); } ~obj() { cout <<"destroy "<<name<<endl; } // Destruktor obj(obj &cp) { // Copykonstruktor cout << "create a copy of "<<cp.name<<endl; strcpy(name, cp.name); } obj operator+(obj &a) { // Operator + cout <<name<<".operator+("<<a.name<<")"<<endl; char buf[20]; sprintf(buf, "%s+%s", name, a.name); obj temp(buf); return temp; // Kopie des temp. Objekts } obj &operator=(obj &a) { // Operator = cout <<name<<".operator=("<<a.name<<")"<<endl; strcpy(name, a.name); return *this; // Referenz auf akt. Objekt } };

Page 103: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-103

main() { obj a("a"), b( "b"); a = b; // operator+ klappt ohne Probleme //a = a+b+a; // Fehler bei operator= auf temp. Objekt //a = a+(b+a); // Fehler bei operator+ auf temp. Objekt cout <<"fetich mit Zuweisung!"<<endl; } Die Zuweisung a = b ; klappt anstandslos. Bei a = a+b+a; kommt die Compilerfehlermeldung: opera.cc: In function `int main()': opera.cc:39: initialization of non - const reference type `class obj &' opera.cc:39: from rvalue of type `obj' opera.cc:27: in passing argument 1 of `o bj::operator =(obj &)' Hier wird versucht, eine Referenz auf ein konstantes Objekt an die Funktion operator=() zu übergeben, was natürlich scheitert. Deshalb sollte der Referenzoperator aus der Deklaration entfernt werden: obj &operator=(obj a) { // Operator = Behebt man das Problem, dann gibt das Programm die folgende Ablaufmeldung aus: thales$ try opera.cc create a create b a.operator+(b) create a+b create a copy of a+b destroy a+b a+b.operator+(a) create a+b+a create a copy of a+b+a des troy a+b+a a.operator=(a+b+a) destroy a+b+a destroy a+b fetich mit Zuweisung! destroy b destroy a+b+a Wenn man nun versucht, die Zeile a = a+(b+a); zu kompili eren, läuft man in das gleiche Problem bei operator+() : opera.cc: In function `int main()': oper a.cc:39: initialization of non - const reference type `class obj &' opera.cc:39: from rvalue of type `obj' opera.cc:20: in passing argument 1 of `obj::operator +(obj &)' Auch hier muss der Referenzoperator bei operator+() entfernt werden:

Page 104: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-104

obj operator+(obj a); Denn es wird versucht, a.operator+(b.operator(a)) zu bilden. Und der Aufruf b.operator(a) liefert ein temporäres Objekt, auf das dann keine Referenz gebildet werden kann.

9.4 Elementinitialisierung beim Konstruktor via ":"-Operator thales$cat init.cc #include <iostream> class A { int x, y; public: // Defaultkonstruktor A(int initx=0, int inity=0):x(initx), y(inity)

{ cout <<"A constructor called!"<<endl; } friend ostream& operator<<(ostream &out, const A& a) { return out << "x="<<a.x<<" y="<<a.y; } }; int main() { A a(5,1); cout <<a<<endl; } thales$ cat objinit.cc // Beispiel fuer die Initialisierung bei Objekten als Klassenelementen #include <iostream> class A { int x, y; public: A(int initx=0, int inity=0) // Defaultkonstruktor { cout <<"A constructor called!"<<endl; x = initx; y = inity; } A(const A&a) { cout <<"A copy constructor called!"<<endl; x = a.x; y = a.y; } A& operator=(const A &a) { cout <<"A operator= called!"<<endl; x = a.x; y = a.y; return *this; } friend ostream& operator<<(ostream &out, const A& a) { return out << "x="<<a.x<<" y="<<a.y; } };

Page 105: Objektorientierte Softwareentwicklung mit C++ · Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 1-5 1 Historische Entwicklung der Programmiersprachen

Objektorientierte Softwareentwicklung mit C++ / WS 2001 / Stand: 05.02.02 Kapitel 9-105

class B { int t, z; A a; public: B(int , int, int, int); friend ostream& operator<<(ostream &out, const B &b) { return out << "B:t="<<b.t<<" z="<<b.z<<" A:"<<b.a; } }; B::B(int initz=0, int initt=0, int initx=0, int inity=0):a(initx,inity) { z = initz; t = initt; } int main() { B b(5,1, 3,4); cout <<b<<endl; }