Skript

Transcription

Skript
Karlheinz Hug
Informatik 3
Skript
Teil 1
Konzepte und Entwürfe
objektorientierter Programmiersprachen
Bachelor-Studiengang
Medien- und Kommunikationsinformatik
Fakultät Informatik
Hochschule Reutlingen
Reutlingen University
27. September 2012
ii
Maskulin – Feminin
Alle Berufs-, Rollen- und Personenbezeichnungen in diesem Skript und begleitenden
Dokumenten, die als generische Maskulina wie Leser, Benutzer, Entwickler geschrieben sind, beziehen sich unabhängig von ihrem grammatischen Geschlecht stets auf
Menschen beiderlei biologischen Geschlechts.
Warenzeichen
BlackBox und Component Pascal sind eingetragene Warenzeichen von Oberon microsystems.
Design by Contract ist ein eingetragenes Warenzeichen von Eiffel Software.
Eiffel ist ein Warenzeichen von Nonprofit International Consortium for Eiffel (NICE).
Java ist ein eingetragenes Warenzeichen von Oracle Corporation.
Visual C# ist ein eingetragenes Warenzeichen von Microsoft Corporation.
Ada, BON, C, C++, Go, HTML, Modula, Oberon, OCL, Pascal, Prolog, Scala, UML,
XML und andere Namen sind vielleicht eingetragene Warenzeichen. Erscheinen in diesem Dokument Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw., so
berechtigt dies nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichenund Markenschutz-Gesetze als frei zu betrachten wären und daher von jedem benutzt
werden dürften.
Garantieverzichtserklärung
Alle in diesem Skript und begleitenden Dokumenten enthaltenen Informationen wie
Texte, Bilder, Programme und Verfahren wurden sorgfältig erstellt und geprüft. Da
Fehler trotzdem nicht auszuschließen sind, ist der Inhalt des Skripts mit keinerlei Verpflichtung oder Garantie verbunden. Der Autor übernimmt weder eine juristische Verantwortung für eventuell verbliebene fehlerhafte Angaben und deren Folgen, noch
irgendeine Haftung für Schäden, die in Zusammenhang mit der Verwendung dieses
Dokuments, der darin dargestellten Methoden und Programme, oder Teilen davon entstehen.
Missbrauchsverbot
Die durch dieses Skript vermittelten Informationen wie Prinzipien, Konzepte, Methoden, Verfahren und Techniken dürfen nicht für militärische, völkerrechtswidrige, rassistische oder sonstige inhumane Zwecke missbraucht werden.
Anschrift des Autors
Prof. Dr. Karlheinz Hug
Hochschule Reutlingen – Reutlingen University
Fakultät Informatik
Studiengang Medien- und Kommunikationsinformatik
Alteburgstraße 150
72762 Reutlingen
Bundesrepublik Deutschland – Germany
Gebäude-Raum
9-126
Telefon
+49/7121/271- 4013
E-Mail
[email protected]
[email protected]
WWW
http://www.mki.reutlingen-university.de
Online-Dienst
http://informatik.karlheinz-hug.de
ftp://studinf.reutlingen-university.de/MKI/Hug
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Vorwort
Aufgabe
1
Bild
1 meiner
1 (1) Sprache
Leitlinie Programm
1bedeuten1die Grenzen
Tabelle
1
DieBeispiel
Grenzen
meiner
Welt.
Ludwig Wittgenstein (1889 – 1951)
Tractatus logico-philosophicus, 5.6
Voraussetzungen
Dieses Skript und das Lehrmodul Informatik 3 setzen Grundkenntnisse der praktischen
Informatik etwa im Umfang der zwei vierstündigen Vorlesungen Informatik 1 und 2
und in begleitenden Praktika erworbene Fähigkeiten im Programmieren mit einer
objektorientierten Programmiersprache voraus. Gefordert sind zudem Grundkenntnisse
der Mathematik, der Logik und der theoretischen Informatik etwa im Umfang der Lehrmodule Theoretische Grundlagen 1 und 2.
Wer nur einen Hammer
kennt, dem erscheint
jedes Ding als Nagel
„Für Informatikabsolventen genügt es, eine gängige Programmiersprache zu beherrschen.“ Einige Informatikstudiengänge folgen diesem engen Ein-Sprachen-Ansatz. Ist
er einer akademischen Informatikausbildung didaktisch angemessen? Von den tausenden entwickelten Programmiersprachen werden viele noch benutzt, und es entstehen
weiter neue, denn jeder Entwicklungsstand ist vorläufig. Ohne Programmiersprachen
ist alles Programmieren nichts, aber Programmiersprachen sind nicht alles. Sie sind nur
Mittel – wie ein Hammer zum Bearbeiten von Metallstücken. Doch kann, wer einen
Hammer greift, schon ein brauchbares, kunstvolles Eisentor schmieden? Erfordert das
nicht vielmehr handwerkliche und kreative Fähigkeiten?
Was?
Ein Informatikstudium, das zu einer 45jährigen Berufspraxis qualifizieren soll, muss
vor allem Kenntnisse und Fähigkeiten mit langer Halbwertszeit vermitteln. So empfiehlt sich ein ganzheitlicher Ansatz, der sich auf Prinzipien, Konzepte, Methoden und
Techniken der Softwareentwicklung konzentriert, denen Mittel und Werkzeuge, also
auch Programmiersprachen, untergeordnet sind. Dieses Skript versucht eine Gratwanderung: Es stellt Konzepte und Entwürfe objektorientierter Programmiersprachen
anhand wichtiger Algorithmen, Datenstrukturen, Entwurfsmuster und Programmiertechniken vor.
Wozu Sprachkonzepte
studieren?
Konzepte von Programmiersprachen zu studieren ist nützlich, weil es diese Fähigkeiten
verbessert [PrZ98] S. 22ff, [Seb06] S. 2ff:
Ideen und Lösungen zu informatischen Problemen formulieren,
die historische Entwicklung von Programmiersprachen verstehen,
die Bedeutung von Sprachimplementationen erkennen,
die aktuelle Programmiersprache effektiv und effizient benutzen,
selbstständig schnell weitere Programmiersprachen lernen,
für eine Anwendung die am besten geeignete Sprache wählen,
neue Sprachen mit geforderten Eigenschaften entwerfen.
Auch die letzte Fähigkeit ist nicht nur von wenigen Informatikern gefordert, denn letztlich definiert jede Benutzungsoberfläche, jede Schnittstelle eine neue Sprache.
Wozu Sprachentwürfe
studieren?
Entwürfe verschiedener Programmiersprachen zu studieren ist insbesondere für
Medien- und Kommunikationsinformatiker interessant, weil
dies nicht nur technisch-pragmatische, sondern auch menschlich-pragmatische,
gestalterische und ästhetische Aspekte umfasst. Wie es mehr oder minder benutzerfreundliche und ansprechende Webauftritte gibt, so gibt es mehr oder minder entwicklerfreundliche und elegante Programmiersprachen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
iii
iv
Programmierparadigma
Vorwort
sich die Informatik ständig neue Anwendungsbereiche erschließt, die neue Programmiersprachen erfordern. So hat etwa das Web zur Entwicklung von HTML,
CSS, XML, Java, JavaScript, JSP, PHP, Curl, Ajax geführt. Sprachen mögen neu
sein, umgesetzte Konzepte sind oft bekannt.
viele Schwächen und Mängel etablierter Programmiersprachen in innovativen Sprachen schon überwunden sind. Allmählich setzen sich Verbesserungen auch in gängigen Sprachen durch, wie die Entwicklung C++ – Java – D – C# – Spec# – Scala –
Go zeigt.
Niemand kennt alle Programmiersprachen. Möglichst viele Details möglichst vieler
Sprachen vermitteln zu wollen, wäre so unmöglich wie unsinnig. Sinnvoll ist dagegen,
die Menge der Programmiersprachen nach verschiedenen Aspekten zu strukturieren
und wiederkehrende Ideen heraus zu abstrahieren. Welche Programmierparadigmen
und Konzepte soll dieses Skript behandeln, welche Entwürfe und Sprachen dazu auswählen? Bjarne Stroustrup rät in einem Interview in [Seb06] S. 507:
„It is essential for anyone who wants to be considered a professional in the areas of
software to know several languages and several programming paradigms. [...] Much
of the inspiration to good programming comes from having learned and appreciated
several programming styles and seen how they can be used in different languages.
Furthermore, I consider programming of any nontrivial program a job for professionals with a solid and broad education, rather than for people with a hurried and
narrow "training."“
Niklaus Wirth akzentuiert in einem Interview in [Seb06] S. 441:
Statisch typisierte
objektorientierte
Mehrzwecksprache
„It is important that CS [computer science, K.H.] students know the various programming paradigms: procedural, functional, logic, and object-oriented. But obviously the procedural style remains closest to the computer on which programs are
interpreted ("run"). [...] The object-oriented style is based on the procedural style; it
is a variant of it, not really different, even if procedures are now called "methods,"
and calling a procedure is termed "sending a message." I consider functional and
logic programming "niche styles" much rather than procedural programming.“
Das Lehrmodul Informatik 3 setzt enge Zeitgrenzen. Deshalb konzentriert sich das
Skript auf die für die Medien- und Kommunikationsinformatik praktisch relevante
objektorientierte Ausprägung des imperativen Paradigmas. Andere Programmierparadigmen bleiben leider ebenso außer Betracht wie frühe imperative Sprachen, Skriptsprachen und aktuelle Sprachentwicklungen im Internet-Bereich – nur 1.2 S. 1-4 gibt
einen Überblick. Das Skript beschränkt sich auf statisch typisierte objektorientierte
Mehrzwecksprachen, die interessante Konzepte zur Entwicklung der Programmiersprachen beigetragen haben oder zurzeit in der industriellen Praxis verbreitet sind.
Programmierpraxis
Die ausgewählten Sprachen dienen im Informatik 3 Praktikum als Implementationssprachen, damit die Studierenden in diesen Sprachen programmieren lernen, die Basis
ihrer praktischen Programmiererfahrung erweitern und so gut vorbereitet in die Praxisphase eintreten können.
Wie?
Der Ansatz dazu ist, von Kenntnissen der ersten Lehrsprache Component Pascal ausgehend Eiffel, C++, Java und C# zu erschließen. Das Skript fokussiert auf gemeinsame,
wesentliche Sprachkonzepte (die meist von Component Pascal bekannt sind) und vergleicht die verschiedenen Entwürfe einzelner Sprachkonzepte. Auf dem Weg von Component Pascal zu rein objektorientiertem Programmieren ist Eiffel nützlich, weil diese
rein objektorientierte Sprache theoretische Grundlagen abstrakter Datentypen und
Klassen reflektiert, objektorientierte Konzepte klar umsetzt und mit der Idee der vertraglichen Spezifikation verbindet, und sich deshalb besonders gut für objektorientierte
Softwareentwicklung eignet. Dagegen sind die Sprachentwürfe für C++, Java und C#
eher pragmatisch vom Wunsch geprägt, sie evolutionär von der weit verbreiteten pro-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Vorwort
v
zeduralen Systemimplementationssprache C hin zu mehr Objektorientierung zu entwickeln, ohne sich zu weit von Traditionen der C-Welt zu entfernen. Deshalb heißen
C++, Java und C# hier kurz C*-Sprachen. Dass sie in einer Entwicklungslinie liegen,
begrenzt den Lernaufwand.
Probleme lösen
Sprachkonzepte und -konstrukte lernt man am besten an Beispielen. In der Literatur
sind „Wegwerfbeispiele“ verbreitet, die keinen Nutzen haben außer dem, bestimmte
Sprachkonstrukte vorzustellen. Dagegen bevorzugt dieses Skript Beispiele, die praktisch relevante Probleme durch Algorithmen, Datenstrukturen, Entwurfsmuster und
Programmiertechniken aus dem Standardwissensschatz von Informatikern lösen.
Zudem stellt es solche Beispiele nicht nur in wesentlichen Fragmenten, sondern in vollständigen ausführbaren Programmen vor. Damit lernen die Studenten nicht nur Sprachen, sondern wichtige Lösungen zu Programmierproblemen kennen.
Vom Groben zum
Feinen
Der Ansatz des Problemlösens bis zum ausführbaren Programm bedingt, dass man sich
früh auch mit Details der einzelnen Sprachen befassen muss. Trotzdem versucht das
Skript, nicht den für Einführungen in das Programmieren im Grundstudium üblichen
Weg von den kleinen Einheiten der Sprachen wie Zeichensätze, Literale, Variablen,
Ausdrücke, Anweisungen zu den großen Einheiten wie Module, Klassen, Pakete zu
gehen, sondern den umgekehrten Weg einzuschlagen: von softwaretechnisch relevanten Strukturierungskonzepten zu nachrangigen Details.
Vergleichen
Bekannten Eigenschaften von Component Pascal stellt das Skript zunächst entsprechende Eigenschaften von Eiffel und C* gegenüber. Der Leser rekapituliert damit
wesentliche Programmierkonzepte, die Component Pascal unterstützt, und erfährt beispielhaft, wie andere Sprachen sie realisieren. Danach befasst sich das Skript mit speziellen Merkmalen der einzelnen Sprachen, die Component Pascal nicht kennt.
Nicht beabsichtigt ist, die Sprachen vollständig darzustellen, zu vergleichen oder zu
bewerten. Versucht man, Programmiersprachen Merkmal für Merkmal zu vergleichen,
so läuft man Gefahr, Sprachelemente aus ihrem Kontext zu isolieren und ihr Zusammenwirken mit anderen Elementen zu ignorieren. Um einer Programmiersprache
gerecht zu werden, muss man ihre Eigenschaften in ihrem Zusammenhang betrachten.
Abstrahieren
Trotz dieser Vorbehalte ist der „vergleichende“ Ansatz als Einstieg in weitere Programmiersprachen gewählt, weil sich damit auch das Allgemeine an konkreten Realisierungen programmiersprachlicher Konzepte verdeutlichen und abstrahieren lässt. Beispielprogramme und -fragmente sollen die Gemeinsamkeiten der Sprachen herausstellen,
nicht unbedingt alle ihre speziellen Merkmale einsetzen. Mit dieser Sichtweise sollte
das selbstständige Lernen weiterer imperativer Programmiersprachen wenig Schwierigkeiten bereiten. Die Fähigkeit dazu ist z.B. wegen der weiten Verbreitung vieler
Skriptsprachen vorteilhaft. Zudem wird sich zeigen, dass die Kenntnis effektiver Entwurfsmuster und effizienter Algorithmen wichtiger ist und sich schwerer selbstständig
aneignen lässt als etwa die Kenntnis des Schleifenkonstrukts in dieser oder der Klassendefinition in jener Sprache.
Struktur des Skripts
Kapitel 1 führt in verschiedene Aspekte von Programmiersprachen ein. Die nächsten
drei Kapitel folgen dem erwähnten Probleme-lösen-Ansatz und bringen Programmbeispiele in den ausgewählten Sprachen Component Pascal, Eiffel, C++, Java und C#,
ausgehend von ganz einfachen in Kapitel 2 bis zu solchen in den Kapiteln 3 und 4, die
Polymorphie, Generizität und Entwurfsmuster demonstrieren. Danach folgen die Kapitel 5 bis 9 dem erwähnten Top-Down-Ansatz.
Literatur
Das Literaturverzeichnis enthält vor allem Verweise auf Lehrbücher, gegliedert in
sprachübergreifende Literatur und Literatur zu einzelnen Sprachen. Literaturhinweise
sind im Text durch Kurzbelege des Literaturverzeichnisses wie [ECMA367] angegeben. Nur einmalig referenzierte Web-Adressen erscheinen dagegen eher in Fußnoten.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
vi
Vorwort
Das Konzept dieses Skripts orientiert sich an der Art von Lehrbüchern, die im Literaturverzeichnis unter der Marginalie Programmiersprachen S. A-3 zu finden sind. Quellen
für vorgestellte Sprachelemente sind vorzüglich originale Sprachbeschreibungen.
Oberon-2 ist in [ReW94], [Mös94] beschrieben, Component Pascal in [Hug01],
[War02]. Die Quellen für Eiffel sind der ECMA-Standard [ECMA367] und der NICEStandard [ER06], [Mey95], [Mey92]. Bei C++ geht das Skript nicht auf Dialekte ein,
sondern orientiert sich am ANSI/ISO-Standard [C++11], [C++98], [Str97], [ElS90].
Referenzen für Java sind [GJSBB11], [GJSB05], [GJSB00], [GJS96], für C#
[ECMA334], [C#PR12], [WiH04], [HWG04].
Das Skript ersetzt keine Einführungen in das Programmieren mit Eiffel oder C*, dazu
stellt es die Sprachelemente zu lückenhaft dar. Zu Eiffel gibt es eine Reihe guter Einführungen. Die Literatur zu den C*-Sprachen ist unüberschaubar groß, sodass dieses
Rad nicht neu zu erfinden ist. Der Leser sei zur begleitenden Lektüre auf Lehrbücher
verwiesen sowie auf die vielen Tutorien, die im Web leicht zu finden sind.
Dokumentationssprache Englisch
Der Text versucht, der neuen deutschen Rechtschreibung zu folgen. Modelle, Spezifikationen und Programme sind englisch formuliert. Die Gründe dafür sind vielschichtig:
Programmiersprachen orientieren sich am Englischen. Englisch ist international verbreitet, in vielen Firmen Dokumentationssprache und wird durch die Globalisierung in
der Praxis von Softwareentwicklern immer wichtiger. Wiederverwendbare Softwarekomponenten müssen englisch dokumentiert sein, um einen Markt zu finden. Außerdem sind englische Wörter oft kürzer als entsprechende deutsche.
Darstellung des Texts
Bezeichnungen besonders wichtiger Begriffe und neu eingeführte Bezeichnungen sind
bei ihrem ersten Auftreten oder ihrer Definition durch Fettdruck hervorgehoben.
Andere wichtige, aber anderswo definierte Bezeichnungen, in der Literatur verwendete
Synonyme und englische Bezeichnungen erscheinen kursiv. Zwecks leichten Erkennens sind auch Namen von Programmiersprachen, Werkzeugen und anderen Produkten
kursiv geschrieben.
Notation für
Programmtext
Programmtexte verwenden die Schriftarten, Formatierungen, Namenskonventionen
und Einrückungsregeln der jeweiligen offiziellen Beschreibung der Programmiersprache. Bei Java und C# sind immer noch Festbreitschriften üblich, während Component
Pascal und Eiffel seit langem besser lesbare Proportionalschriften mit Kursiv- und
Fettstil benutzen. Für C++ ist Festbreitschrift verbreitet, aber Stroustrup verwendet seit
1997 kursive Proportionalschrift [Str97]. Einzelne Zeilen sind mit speziellen Symbolen
markiert:
weist auf etwas Wichtiges oder nachfolgend Erläutertes hin,
ärgert sich über eine mangelhafte Programmstelle oder etwas Nachteiliges,
☺
erfreut sich an der korrigierten Programmstelle oder etwas Vorteilhaftem,
warnt vor einer fehlerhaften Programmstelle oder einem gefährlichen Konstrukt.
Nun wünsche ich dem Leser viel Spaß beim Lesen. Hinweise auf Fehler, Kritik und
Zustimmung nehme ich gerne entgegen.
Reutlingen, den 9. März 2012
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Karlheinz Hug
27.9.12
Inhaltsverzeichnis
Vorwort ....................................................................................................................... iii
Bilderverzeichnis ........................................................................................................xv
Tabellenverzeichnis ................................................................................................. xvii
Programmverzeichnis .............................................................................................. xix
Beispielverzeichnis ...................................................................................................xxv
Leitlinienverzeichnis ............................................................................................. xxvii
Aufgabenverzeichnis .............................................................................................. xxix
1
Einführung
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1.8
Klassifikation von Programmiersprachen ......................................................1
Programmierparadigmen ................................................................................4
1.2.1 Imperatives Programmierparadigma ..................................................5
1.2.2 Funktionales Programmierparadigma ................................................9
1.2.3 Logikbasiertes Programmierparadigma ...........................................12
1.2.4 Regelbasiertes Programmierparadigma ............................................12
1.2.5 Fazit ..................................................................................................13
Abstraktionsebenen in imperativen Sprachen ..............................................14
Soll und Ist von Programmiersprachen ........................................................16
1.4.1 Beschreibung von Programmiersprachen .........................................16
1.4.1.1 Pragmatik .............................................................................16
1.4.1.2 Semantik ...............................................................................17
1.4.1.3 Syntax ...................................................................................17
1.4.2 Implementation von Programmiersprachen .....................................18
1.4.2.1 Darstellung in der Zielsprache .............................................18
1.4.2.2 Werkzeuge ............................................................................19
1.4.2.3 Implementationskonzepte .....................................................19
1.4.3 Vielfalt und Einheit von Programmiersprachen ...............................20
1.4.4 Umgebungen von Programmiersprachen .........................................20
Einflüsse auf und von Programmiersprachen ...............................................20
1.5.1 Einflüsse auf Programmiersprachen .................................................20
1.5.2 Einflüsse von Programmiersprachen ................................................20
Entwicklung imperativer Sprachen ..............................................................21
Merkmale von Programmiersprachen ..........................................................23
1.7.1 Nichttechnische Merkmale ...............................................................23
1.7.2 Technische Merkmale ......................................................................23
1.7.3 Qualitätsmerkmale von Sprachbeschreibungen ...............................23
1.7.4 Qualitätsmerkmale von Sprachimplementationen ...........................24
Überblick über die Auswahlsprachen ...........................................................25
1.8.1 Ziele der Sprachentwürfe .................................................................25
1.8.2 Quantitative Eigenschaften ..............................................................27
1.8.3 Grundstrukturen von Programmen ...................................................28
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
vii
viii
Inhaltsverzeichnis
1.8.4
1.8.5
1.8.6
1.8.7
2
Einmal quer durch die Sprachen
2.1
2.2
2.3
2.4
2.5
3
Unterstützte Programmierstile, Abstraktionsebenen, Sprachkonzepte
...........................................................................................................29
Sprachdefinitionen und Syntaxbeschreibungen ...............................32
Sprachimplementationen ..................................................................32
Zielsprachen und Übersetzungskonzepte .........................................33
Hello-World-Programme ................................................................................1
2.1.1 Component Pascal ..............................................................................1
2.1.2 Eiffel ...................................................................................................2
2.1.3 C++ .....................................................................................................3
2.1.4 Java .....................................................................................................8
2.1.5 C# .....................................................................................................10
2.1.6 Weitere Sprachen ..............................................................................11
2.1.7 Fazit ..................................................................................................12
Argumentübernahme von der Kommandozeile ............................................13
2.2.1 Component Pascal ............................................................................13
2.2.1.1 Argumentübernahme ............................................................13
2.2.1.2 Konversion ...........................................................................14
2.2.2 Eiffel .................................................................................................14
2.2.2.1 Argumentübernahme ............................................................14
2.2.2.2 Konversion ...........................................................................16
2.2.2.3 Entfernte Argumentübernahme ............................................17
2.2.3 C++ ...................................................................................................19
2.2.3.1 Argumentübernahme ............................................................19
2.2.3.2 Konversion ...........................................................................21
2.2.4 Java ...................................................................................................21
2.2.4.1 Argumentübernahme ............................................................21
2.2.4.2 Konversion ...........................................................................22
2.2.5 C# .....................................................................................................23
2.2.5.1 Argumentübernahme ............................................................23
2.2.5.2 Konversion ...........................................................................24
2.2.6 Fazit ..................................................................................................25
Uhrzeit ..........................................................................................................26
2.3.1 Component Pascal ............................................................................27
2.3.2 Eiffel .................................................................................................29
2.3.3 C++ ...................................................................................................31
2.3.4 Java ...................................................................................................34
2.3.5 C# .....................................................................................................35
2.3.6 Fazit ..................................................................................................37
Aufgaben ......................................................................................................38
Lösungen ......................................................................................................41
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
3.1
Größter gemeinsamer Teiler zweier Ganzzahlen – funktional .......................1
3.1.1 Funktionale Sprachen .........................................................................2
3.1.2 Component Pascal ..............................................................................4
3.1.3 Eiffel ...................................................................................................8
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Inhaltsverzeichnis
ix
3.2
3.3
27.9.12
3.1.4 C++ ...................................................................................................11
3.1.5 Java ...................................................................................................12
3.1.6 C# .....................................................................................................13
3.1.7 Fazit ..................................................................................................14
3.1.8 Aufgaben ..........................................................................................15
Größter gemeinsamer Teiler – objektorientiert .............................................17
3.2.1 Entwurf .............................................................................................18
3.2.2 Eiffel .................................................................................................19
3.2.2.1 Schnittstellenklasse ..............................................................19
3.2.2.2 Implementationsklassen .......................................................20
3.2.2.3 Kundenklasse .......................................................................22
3.2.3 C++ ...................................................................................................24
3.2.3.1 Schnittstellenklasse ..............................................................24
3.2.3.2 Implementationsklassen .......................................................24
3.2.3.3 Kundenfunktion ....................................................................27
3.2.4 Java ...................................................................................................28
3.2.4.1 Schnittstellenklasse ..............................................................29
3.2.4.2 Implementationsklassen .......................................................29
3.2.4.3 Kundenklasse .......................................................................30
3.2.5 C# .....................................................................................................31
3.2.5.1 Schnittstellenklassen ............................................................31
3.2.5.2 Implementationsklassen .......................................................32
3.2.5.3 Kundenklasse .......................................................................33
3.2.6 Fazit ..................................................................................................34
3.2.7 Aufgaben ..........................................................................................34
Abstraktion mit Konzeptklassen und Schablonenmethoden ........................35
3.3.1 Teile und herrsche – funktional ........................................................36
3.3.2 Objektorientierter Entwurf der abstrakten Problemlösung nur mit
Abfragen ...........................................................................................38
3.3.3 Eiffel – Konzeptklassen ....................................................................38
3.3.4 Objektorientierter Entwurf der konkreten Problemlösung ...............42
3.3.5 Eiffel .................................................................................................42
3.3.5.1 Schnittstellenklassen ............................................................42
3.3.5.2 Implementationsklassen .......................................................44
3.3.6 Objektorientierter Entwurf mit Abfragen und Aktionen ..................46
3.3.7 Eiffel .................................................................................................47
3.3.7.1 Konzeptklasse .......................................................................47
3.3.7.2 Implementationsklassen .......................................................48
3.3.7.3 Kundenklasse .......................................................................49
3.3.8 C++ ...................................................................................................51
3.3.8.1 Konzeptklassen .....................................................................51
3.3.8.2 Implementationsklassen .......................................................52
3.3.8.3 Kundenfunktion ....................................................................55
3.3.9 Java ...................................................................................................55
3.3.9.1 Konzeptklassen .....................................................................55
3.3.9.2 Implementationsklassen .......................................................56
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
x
Inhaltsverzeichnis
3.3.9.3 Kundenklasse .......................................................................57
3.3.10 C# .....................................................................................................57
3.3.10.1 Konzeptklassen ...................................................................57
3.3.10.2 Implementationsklassen .....................................................58
3.3.10.3 Kundenklasse .....................................................................59
3.3.11 Fazit ..................................................................................................59
3.3.12 Aufgaben ..........................................................................................60
4
Reihungen und Abstraktionen
4.1
4.2
4.3
4.4
Binäres Suchen in einer sortierten Reihung ...................................................1
4.1.1 Trennen wiederverwendbarer Komponenten von Anwendungen ......2
4.1.2 Routinen schachteln oder nicht? ........................................................2
4.1.3 Component Pascal ..............................................................................2
4.1.4 Eiffel ...................................................................................................6
4.1.4.1 Werkzeugkasten ......................................................................6
4.1.4.2 Testtreiber ...............................................................................8
4.1.5 C++ ...................................................................................................10
4.1.5.1 Werkzeugkasten: Namensraum und generische Funktionen 10
4.1.5.2 Werkzeugkasten: Generische Klasse und Klassenfunktionen
...........................................................................................14
4.1.5.3 Testtreiber .............................................................................16
4.1.6 Java ...................................................................................................17
4.1.6.1 Werkzeugkasten ....................................................................17
4.1.6.2 Testtreiber .............................................................................19
4.1.7 C# .....................................................................................................20
4.1.8 Fazit ..................................................................................................21
Sortieren einer Reihung mit Quicksort .........................................................22
4.2.1 Component Pascal ............................................................................23
4.2.2 Eiffel .................................................................................................24
4.2.3 C++ ...................................................................................................24
4.2.4 Java ...................................................................................................25
4.2.5 C# .....................................................................................................25
4.2.6 Fazit ..................................................................................................25
Indexbereich mit maximaler Summe ............................................................25
4.3.1 Ersetzen von Ausgabeparametern ....................................................25
4.3.2 Component Pascal ............................................................................26
4.3.3 Eiffel .................................................................................................27
4.3.4 C++ ...................................................................................................29
4.3.5 Java ...................................................................................................31
4.3.6 C# .....................................................................................................31
4.3.7 Fazit ..................................................................................................31
Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen .....32
4.4.1 Eiffel .................................................................................................32
4.4.1.1 Initialisierer ..........................................................................32
4.4.1.2 Testtreiber .............................................................................37
4.4.2 C++ ...................................................................................................42
4.4.2.1 Initialisierer ..........................................................................43
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Inhaltsverzeichnis
xi
4.4.3
4.4.4
4.4.5
5
Programmeinheiten und -strukturen
5.1
5.2
5.3
5.4
6
Kompilationsarten und -einheiten ..................................................................1
5.1.1 Component Pascal ..............................................................................2
5.1.2 Eiffel ...................................................................................................3
5.1.3 C++ .....................................................................................................3
5.1.4 Java .....................................................................................................4
5.1.5 C# .......................................................................................................4
Zusammenfassung und Kapselung von Programmteilen ...............................5
5.2.1 Component Pascal ..............................................................................5
5.2.2 Eiffel ...................................................................................................6
5.2.3 C++ .....................................................................................................6
5.2.4 Java .....................................................................................................8
5.2.5 C# .......................................................................................................8
Geheimnisprinzip, Schutz und Zugriffsrechte ................................................9
5.3.1 Component Pascal ..............................................................................9
5.3.2 Eiffel .................................................................................................10
5.3.3 C++ ...................................................................................................10
5.3.4 Java ...................................................................................................11
5.3.5 C# .....................................................................................................11
Module und Datenabstraktion ......................................................................14
5.4.1 Component Pascal ............................................................................14
5.4.2 Eiffel .................................................................................................15
5.4.3 C++ ...................................................................................................15
5.4.3.1 Prämodule .............................................................................15
5.4.3.2 Namensräume .......................................................................15
5.4.3.3 Klassenmethoden .................................................................17
5.4.4 Java, C# ............................................................................................17
Klassen und objektorientierte Konzepte
6.1
6.2
6.3
6.4
6.5
27.9.12
4.4.2.2 Testtreiber .............................................................................44
Java ...................................................................................................46
C# .....................................................................................................46
Fazit ..................................................................................................46
Abstrakte Datentypen .....................................................................................1
Klassen ............................................................................................................1
6.2.1 Bestandteile von Klassen ...................................................................4
6.2.2 Verhältnis zu Kompilationseinheiten .................................................4
Objekte ............................................................................................................5
6.3.1 Beispiel für Wertobjekt ......................................................................5
6.3.2 Beispiel für Referenzobjekt ................................................................6
6.3.3 Zugriffe auf Objektmerkmale .............................................................6
Klassendaten und -operationen .......................................................................7
6.4.1 Klassendaten .......................................................................................7
6.4.2 Klassenoperationen ............................................................................8
Abstrakte Klassen ...........................................................................................9
6.5.1 Gemeinsames .....................................................................................9
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
xii
Inhaltsverzeichnis
6.5.2 Unterschiedliches .............................................................................10
6.6 Vererbung ......................................................................................................11
6.7 Anpassung geerbter Merkmale .....................................................................12
6.8 Polymorphie und dynamisches Binden ........................................................13
6.8.1 Gemeinsames ...................................................................................13
6.8.2 Historisches ......................................................................................14
6.8.3 Component Pascal ............................................................................14
6.8.4 Eiffel .................................................................................................14
6.8.5 C++ ...................................................................................................15
6.8.6 Java ...................................................................................................17
6.8.7 C# .....................................................................................................18
6.9 Redefinitionsregeln .......................................................................................19
6.10 Zugriffe auf Oberklassenversionen geerbter Operationen ............................19
6.11 Schachtelung .................................................................................................20
6.12 Speicherverwaltung ......................................................................................21
7
Typen und Datenkonzepte
7.1
7.2
7.3
7.4
7.5
7.6
7.7
7.8
7.9
7.10
7.11
7.12
7.13
7.14
7.15
7.16
7.17
7.18
7.19
7.20
7.21
8
Gemeinsamkeiten ...........................................................................................1
Unterschiede ...................................................................................................1
Typsystem .......................................................................................................1
Sprachdefinierte Typen mit Operatoren ..........................................................2
Programmiererdefinierte Typen ......................................................................6
7.5.1 Operatoren ..........................................................................................7
Typdeklarationen, Typdefinitionen .................................................................7
Typgleichheit, Typäquivalenz .........................................................................8
Wertprüfungen zur Laufzeit ............................................................................8
Implizite Typanpassung ..................................................................................8
Explizite Typanpassung, Typkonversion ........................................................9
Statischer und dynamischer Typ .....................................................................9
Typinformationen zur Laufzeit .....................................................................10
Typprüfungen zur Laufzeit ...........................................................................11
Typkonversionen zur Laufzeit ......................................................................11
Zeiger ............................................................................................................12
Reihungen .....................................................................................................12
Zeichenketten ................................................................................................14
Verbunde .......................................................................................................14
Mengen .........................................................................................................15
Einfache Typen .............................................................................................15
7.20.1 Zeichen und Zahlen ..........................................................................15
7.20.2 Boolesche Daten ...............................................................................16
7.20.3 Aufzählungen ...................................................................................16
Konstanten ....................................................................................................17
Ablaufsteuerung und algorithmische Konzepte
8.1
Routinen ..........................................................................................................1
8.1.1 Prozeduren ..........................................................................................1
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Inhaltsverzeichnis
xiii
8.1.2
8.1.3
8.1.4
8.1.5
8.1.6
8.2
8.3
8.4
9
Funktionen ..........................................................................................2
Parameter- und Ergebnisübergabe ......................................................3
Eigenschaften von Routinen ...............................................................5
Routinentypen und -variablen ............................................................6
Was entspricht den vordeklarierten Component-Pascal-Prozeduren?
.............................................................................................................7
8.1.7 Was entspricht den vordeklarierten Component-Pascal-Funktionen?
.............................................................................................................9
Zusicherungen ..............................................................................................12
Anweisungen ................................................................................................14
8.3.1 Übersicht ..........................................................................................14
8.3.2 Syntaktischer Zucker ........................................................................15
8.3.3 Semantisches Salz ............................................................................15
8.3.4 Zuweisungen ....................................................................................16
8.3.5 Auswahlanweisungen .......................................................................16
8.3.5.1 Alternative Bedingungen ......................................................16
8.3.5.2 Alternative Werte .................................................................17
8.3.6 Wiederholungsanweisungen .............................................................18
Testhilfen ......................................................................................................19
Syntaktische und lexikalische Aspekte
9.1
9.2
9.3
9.4
Kommentare ...................................................................................................1
Zeichencodes ..................................................................................................1
Bezeichner ......................................................................................................1
Wortsymbole ...................................................................................................2
A Literaturverzeichnis
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
xiv
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Inhaltsverzeichnis
27.9.12
Bilderverzeichnis
1
Einführung
Bild 1.1 Entwickler – Programm – Rechner ............................................................1
Bild 1.2 Hierarchie virtueller Rechner .....................................................................4
Bild 1.3 Zustandsübergangsdiagramm ....................................................................6
Bild 1.4 Struktogramm ............................................................................................6
Bild 1.5 Prozeduren mit Benutzung und Schachtelung ...........................................7
Bild 1.6 Module mit Benutzung und Schachtelung .................................................7
Bild 1.7 Abstrakter Datentyp mit Exemplaren ........................................................8
Bild 1.8 Klassen mit Benutzung und Vererbung .....................................................8
Bild 1.9 Komponenten .............................................................................................9
Bild 1.10 Generische Einheit mit Konkretisierungen ..............................................9
Bild 1.11 Aspekte von Programmiersprachen ......................................................16
Bild 1.12 Pragmatik ...............................................................................................17
Bild 1.13 Entwicklungslinien imperativer Programmiersprachen ........................22
2
Einmal quer durch die Sprachen
Bild 2.1 Inkludierungsbeziehungen zu Programm 2.7 ...........................................6
Bild 2.2 Benutzen und beerben ..............................................................................17
Bild 2.3 Kunden-Lieferanten-Kette .......................................................................18
Bild 2.4 Feigenbaum-Diagramme ........................................................................40
3
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Bild 3.1 Kunde mit Schnittstellenklasse und Implementationsklassen .................18
Bild 3.2 Abstraktes Problem mit abstrakter Lösung ..............................................35
Bild 3.3 Konzeptklassen für rekursiv teilbares Problem nur mit Abfragen .........38
Bild 3.4 Konzept-, Schnittstellen- und Implementationsklassen für ggT-Problem
nur mit Abfragen ....................................................................................................42
Bild 3.5 Konzept- und Implementationsklassen für ggT-Problem mit Abfragen und
Aktionen .................................................................................................................47
4
Reihungen und Abstraktionen
Bild 4.1 Modulares Trennen verschiedener Aspekte ...............................................2
Bild 4.2 Klassendiagramm zu Werkzeugkastenklassen .........................................28
Bild 4.3 Klassendiagramm zur Initialisierung von Reihungen ..............................32
Bild 4.4 Klassendiagramm zur Testtreibern .........................................................37
5
Programmeinheiten und -strukturen
6
Klassen und objektorientierte Konzepte
7
Typen und Datenkonzepte
8
Ablaufsteuerung und algorithmische Konzepte
9
Syntaktische und lexikalische Aspekte
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
xv
xvi
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Bilderverzeichnis
27.9.12
Tabellenverzeichnis
1
2
3
4
5
6
7
8
9
Einführung
Tabelle 1.1 Funktional: Fläche eines Rechtecks ...................................................11
Tabelle 1.2 Klassifikation der Auswahlsprachen .................................................25
Tabelle 1.3 Quantitative Eigenschaften ................................................................27
Tabelle 1.4 Minimale Programme ........................................................................29
Tabelle 1.5 Unterstützte Programmierstile ...........................................................30
Tabelle 1.6 Erreichte Abstraktionsebenen ............................................................31
Tabelle 1.7 Unterscheidende Sprachmerkmale ....................................................31
Einmal quer durch die Sprachen
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Reihungen und Abstraktionen
Tabelle 4.1 Ersetzen von Parametern durch globale Variablen .............................26
Programmeinheiten und -strukturen
Tabelle 5.1 Physische Einheiten .............................................................................2
Tabelle 5.2 Kapselung kleiner und großer Programmteile .....................................5
Tabelle 5.3 Schutz, Rechte und Verbote .................................................................9
Tabelle 5.4 Schutz und Zugriffsrechte bei Klassen ..............................................13
Klassen und objektorientierte Konzepte
Tabelle 6.1 Bezeichnungsweisen ............................................................................3
Tabelle 6.2 Eigenschaften von Klassen ..................................................................4
Tabelle 6.3 Wert- und Referenzsemantik ...............................................................5
Tabelle 6.4 Zugriffe auf Objektmerkmale ..............................................................7
Tabelle 6.5 Vererbung ...........................................................................................11
Tabelle 6.6 Schachtelung ......................................................................................21
Typen und Datenkonzepte
Tabelle 7.1 Vordefinierte Typen .............................................................................2
Tabelle 7.2 Programmiererdefinierte Typen ...........................................................6
Ablaufsteuerung und algorithmische Konzepte
Tabelle 8.1 Routinen ................................................................................................1
Tabelle 8.2 Parameter- und Ergebnisübergabe .......................................................3
Tabelle 8.3 Eigenschaften von Routinen ................................................................5
Tabelle 8.4 Was entspricht den vordeklarierten Component-Pascal-Prozeduren? .8
Tabelle 8.5 Was entspricht den vordeklarierten Component-Pascal-Funktionen? .9
Tabelle 8.6 Anweisungen .....................................................................................14
Syntaktische und lexikalische Aspekte
A Literaturverzeichnis
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
xvii
xviii
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Tabellenverzeichnis
27.9.12
Programmverzeichnis
1
2
Einführung
Einmal quer durch die Sprachen
Programm 2.1 Component Pascal: Hello World ....................................................1
Programm 2.2 Eiffel: Hello World 1 ......................................................................2
Programm 2.3 Eiffel: Hello World 2 ......................................................................2
Programm 2.4 C++: Hello World 1 ........................................................................3
Programm 2.5 C++: Hello World 2 ........................................................................4
Programm 2.6 C++: Hello World 3 mit Klasse ......................................................6
Programm 2.7 C++: Hello World 4 mit wiederverwendbarer Klasse ....................7
Programm 2.8 Java: Hello World 1 ........................................................................8
Programm 2.9 Java: Hello World 2 ........................................................................9
Programm 2.10 Java: Hello World 3 ....................................................................10
Programm 2.11 HTML: Applet-Aufruf HelloWorld3 ..........................................10
Programm 2.12 C#: Hello World 1 ......................................................................10
Programm 2.13 C#: Hello World 2 ......................................................................11
Programm 2.14 D: Hello World ...........................................................................11
Programm 2.15 Scala: Hello World 1 ..................................................................11
Programm 2.16 Scala: Hello World 2 ..................................................................11
Programm 2.17 Scala: Hello World 3 ..................................................................12
Programm 2.18 Go: Hello World .........................................................................12
Programm 2.19 Component Pascal: Parameterübernahme ..................................13
Programm 2.20 Eiffel: Argumentübernahme an der Wurzel ................................15
Programm 2.21 Eiffel: Argumentübernahme mit Mix-In ................................17
Programm 2.22 Eiffel: Argumentübernahme irgendwo ohne Mix-In...................18
Programm 2.23 Eiffel: Wurzelklasse zur Argumentausgabe ................................19
Programm 2.24 C++: Argumentübernahme .........................................................20
Programm 2.25 Java: Argumentübernahme 1 ......................................................22
Programm 2.26 Java: Argumentübernahme 2 ......................................................22
Programm 2.27 C#: Argumentübernahme 1 ........................................................23
Programm 2.28 C#: Argumentübernahme 2 ........................................................24
Programm 2.29 Component Pascal: Uhrzeit mit Empfänger-Referenzen ...........27
Programm 2.30 Component Pascal: Uhrzeit mit Empfänger-Zeigern .................28
Programm 2.31 Eiffel: Uhrzeit nicht expandiert ...................................................29
Programm 2.32 Eiffel: Uhrzeit expandiert ...........................................................31
Programm 2.33 Eiffel: Uhrzeit als expandierter Nachfolger von Programm 2.31
.................................................................................................................................31
Programm 2.34 Eiffel: Uhrzeit als nicht expandierter Nachfolger von Programm
2.32 .........................................................................................................................31
Programm 2.35 C++: Uhrzeit, Schnittstelle .........................................................32
Programm 2.36 C++: Uhrzeit, Implementation ....................................................33
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
xix
xx
Programmverzeichnis
3
Programm 2.37 Java: Uhrzeit ...............................................................................35
Programm 2.38 C#: Uhrzeit als Referenztyp .......................................................36
Programm 2.39 C#: Uhrzeit als Werttyp ..............................................................37
Programm 2.40 C++: Schwache Schnittstelle und Implementation ............38
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Programm 3.1 Funktionaler Pseudocode: Größter gemeinsamer Teiler ................2
Programm 3.2 Scheme: Größter gemeinsamer Teiler ............................................3
Programm 3.3 ML: Größter gemeinsamer Teiler 1 ................................................3
Programm 3.4 ML: Größter gemeinsamer Teiler 2 ................................................3
Programm 3.5 Miranda: Größter gemeinsamer Teiler 1 ........................................3
Programm 3.6 Miranda: Größter gemeinsamer Teiler 2 ........................................3
Programm 3.7 Prolog: Größter gemeinsamer Teiler ..............................................4
Programm 3.8 Component Pascal: Größter gemeinsamer Teiler, rekursiv ............4
Programm 3.9 Component Pascal: Größter gemeinsamer Teiler, rekursiv ........4
Programm 3.10 Component Pascal: Größter gemeinsamer Teiler, umgeformt .....6
Programm 3.11 Component Pascal: Größter gemeinsamer Teiler, vereinfacht .....6
Programm 3.12 Component Pascal: Größter gemeinsamer Teiler, iterativ ............7
Programm 3.13 Python: Größter gemeinsamer Teiler mit kollektiver Zuweisung 7
Programm 3.14 Component Pascal: Rechner für größten gemeinsamen Teiler ....8
Programm 3.15 Eiffel: Größter gemeinsamer Teiler, spezifiziert ..........................8
Programm 3.16 Eiffel: Größter gemeinsamer Teiler, rekursiv ...............................8
Programm 3.17 Eiffel: Größter gemeinsamer Teiler, iterativ..................................9
Programm 3.18 Eiffel: Rechner für größten gemeinsamen Teiler .......................10
Programm 3.19 C++: Größter gemeinsamer Teiler, rekursiv ...............................11
Programm 3.20 C++: Größter gemeinsamer Teiler, fast funktional .....................11
Programm 3.21 C++: Größter gemeinsamer Teiler, iterativ ................................11
Programm 3.22 C++: Rechner für größten gemeinsamen Teiler .........................12
Programm 3.23 Java: Rechner für größten gemeinsamen Teiler .........................12
Programm 3.24 Java: Ausnahmen bei Fehleingaben
....................................13
Programm 3.25 C#: Rechner für größten gemeinsamen Teiler ............................14
Programm 3.26 C++: ggT-Funktion 1 ..................................................................15
Programm 3.27 C++: ggT-Funktion 2 ..................................................................15
Programm 3.28 C++: ggT-Funktion 3 ..................................................................15
Programm 3.29 C++: ggT-Funktion 4 ..................................................................15
Programm 3.30 C++: ggT-Funktion 5 ..................................................................16
Programm 3.31 C++: Beispiel von einer IBM-Webseite .................................17
Programm 3.32 C++: Beispiel aus der Prüfung Informatik 3 WS 2006/07 .....17
Programm 3.33 Eiffel: ggT, objektorientiert abstrakt ..........................................19
Programm 3.34 Eiffel: ggT, oo-endrekursiv mit Referenzsemantik ....................20
Programm 3.35 Eiffel: ggT, oo-iterativ mit Referenzsemantik ............................21
Programm 3.36 Eiffel: ggT-Rechner, objektorientiert ..........................................22
Programm 3.37 C++: ggT, objektorientiert abstrakt, Schnittstelle .......................24
Programm 3.38 C++: ggT, objektorientiert abstrakt, partielle Implementation ...24
Programm 3.39 C++: ggT, oo-endrekursiv, Schnittstelle .....................................25
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Programmverzeichnis
xxi
Programm 3.40 C++: ggT, oo-endrekursiv, Implementation mit Wertsemantik ..25
Programm 3.41 C++: ggT, oo-endrekursiv, Implementation mit Zeigersemantik
.................................................................................................................................26
Programm 3.42 C++: ggT, oo-iterativ, Schnittstelle ............................................27
Programm 3.43 C++: ggT, oo-iterativ, Implementation .......................................27
Programm 3.44 C++: ggT-Rechner, objektorientiert ............................................28
Programm 3.45 Java: ggT, objektorientiert abstrakt ............................................29
Programm 3.46 Java: ggT, oo-endrekursiv ..........................................................29
Programm 3.47 Java: ggT, oo-iterativ ..................................................................30
Programm 3.48 Java: ggT-Rechner, objektorientiert ...........................................30
Programm 3.49 C#: ggT, objektorientiert abstrakt, Referenztyp ..........................31
Programm 3.50 C#: ggT, objektorientiert abstrakt, Werttyp ................................32
Programm 3.51 C#: ggT, oo-endrekursiv, Referenztyp.........................................32
Programm 3.52 C#: ggT, oo-iterativ, Werttyp ......................................................32
Programm 3.53 C#: ggT-Rechner, objektorientiert ..............................................33
Programm 3.54 Pseudocode: Problemlöser .........................................................36
Programm 3.55 Funktionaler Pseudocode: Endrekursiv gelöstes Problem .........37
Programm 3.56 Funktionaler Pseudocode: ggT als Konkretisierung ..................37
Programm 3.57 Eiffel: Generische Konzeptklasse für Problem ..........................38
Programm 3.58 Eiffel: Generische Konzeptklasse für teilbares Problem mit
Abfragen ................................................................................................................39
Programm 3.59 Eiffel: Generische Konzeptklasse für endrekursiv gelöstes
Problem ..................................................................................................................40
Programm 3.60 Eiffel: Schablonenmethode zu endrekursiv gelöstem Problem,
umformungsbereit ..................................................................................................40
Programm 3.61 Pseudo-Eiffel: Schablonenmethode zu endrekursiv gelöstem
Problem, umgeformt ..............................................................................................41
Programm 3.62 Eiffel: Generische Konzeptklasse für iterativ gelöstes Problem 41
Programm 3.63 Eiffel: Lösung zum ggT-Problem ...............................................43
Programm 3.64 Eiffel: ggT-Problem, abstrakt .....................................................44
Programm 3.65 Eiffel: ggT-Problem Variante 1 ...................................................44
Programm 3.66 Eiffel: ggT-Problem Variante 2....................................................45
Programm 3.67 Eiffel: Konzeptklasse für iterativ-imperativ gelöstes Problem ...47
Programm 3.68 Eiffel: Lösung zum ggT-Problem, erweitert ...............................48
Programm 3.69 Eiffel: ggT-Problem Variante 3 ...................................................48
Programm 3.70 Eiffel: ggT-Rechner als Problemlöser ........................................49
Programm 3.71 C++: Generische Konzeptklasse für Problem ............................51
Programm 3.72 C++: Generische Konzeptklasse für iterativ-imperativ gelöstes
Problem, Schnittstelle ............................................................................................51
Programm 3.73 C++: Lösung zum ggT-Problem, Schnittstelle ...........................53
Programm 3.74 C++: Lösung zum ggT-Problem, Implementation .....................53
Programm 3.75 C++: ggT-Problem, Schnittstelle ................................................53
Programm 3.76 C++: ggT-Problem, Implementation ...........................................54
Programm 3.77 C++: ggT-Rechner als Problemlöser ..........................................55
Programm 3.78 Java: Generisches Konzept-Interface für Problem .....................55
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
xxii
Programmverzeichnis
4
Programm 3.79 Java: Generische Konzeptklasse für iterativ-imperativ gelöstes
Problem ..................................................................................................................56
Programm 3.80 Java: Lösung zum ggT-Problem .................................................56
Programm 3.81 Java: ggT-Problem ......................................................................56
Programm 3.82 Java: ggT-Rechner als Problemlöser ..........................................57
Programm 3.83 C#: Generische Konzeptklasse für Problem ...............................57
Programm 3.84 C#: Generische Konzeptklasse für iterativ-imperativ gelöstes
Problem ..................................................................................................................58
Programm 3.85 C#: Lösung zum ggT-Problem ...................................................58
Programm 3.86 C#: ggT-Problem ........................................................................58
Programm 3.87 C#: ggT-Rechner als Problemlöser .............................................59
Reihungen und Abstraktionen
Programm 4.1 Component Pascal: Sortiertprüfung und binäres Suchen, rekursiv 3
Programm 4.2 Component Pascal: Partielle Sortiertprüfung, iterativ ....................4
Programm 4.3 Component Pascal: Binäres Suchen, iterativ, unstrukturiert ...........5
Programm 4.4 Eiffel: Sortiertprüfung iterativ; binäres Suchen, rekursiv ...............7
Programm 4.5 Eiffel: Testtreiber zu binärem Suchen ............................................9
Programm 4.6 C++: Sortiertprüfung, iterativ; binäres Suchen, rekursiv;
Schnittstelle mit generischen Funktionen ..............................................................12
Programm 4.7 C++: Sortiertprüfung, iterativ; binäres Suchen, rekursiv;
Schnittstelle mit generischer Klasse ......................................................................15
Programm 4.8 C++: Testtreiber zu binärem Suchen mit generischen Funktionen
.................................................................................................................................16
Programm 4.9 C++: Testtreiber zu binärem Suchen mit generischer Klasse .......17
Programm 4.10 Java: Sortiertprüfung, iterativ; binäres Suchen, rekursiv ............18
Programm 4.11 Java: Testtreiber zu binärem Suchen ..........................................19
Programm 4.12 Miranda: Quicksort .....................................................................22
Programm 4.13 Component Pascal: Quicksort, rekursiv .....................................23
Programm 4.14 C++: Quicksort, rekursiv, Schnittstelle mit generischen
Funktionen .............................................................................................................24
Programm 4.15 Component Pascal: Indexbereich mit maximaler Summe .........27
Programm 4.16 Eiffel: Indexbereich mit maximaler Summe ..............................28
Programm 4.17 C++: Indexbereich mit maximaler Summe, Schnittstelle ..........30
Programm 4.18 Java: Indexbereich mit maximaler Summe ................................31
Programm 4.19 Eiffel: Abstrakte generische Initialisierung von Reihungen
vergleichbarer Elemente .........................................................................................33
Programm 4.20 Eiffel: Initialisierung gleitpunktzahliger Reihungen ..................34
Programm 4.21 Eiffel: Initialisierung von Reihungen von Zeichenketten ..........35
Programm 4.22 Eiffel: Abstrakter generischer Testtreiber zu Reihungen
vergleichbarer Elemente ........................................................................................37
Programm 4.23 Eiffel: Abstrakter Testtreiber zu Reihungen von Zeichenketten .38
Programm 4.24 Eiffel: Abstrakter Testtreiber zu gleitpunktzahligen Reihungen 39
Programm 4.25 Eiffel: Testtreiber zu binärem Suchen in Reihungen von
Zeichenketten .........................................................................................................40
Programm 4.26 Eiffel: Testtreiber zu Indexbereich mit maximaler Summe in
gleitpunktzahligen Reihungen ...............................................................................41
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Programmverzeichnis
xxiii
5
6
7
8
9
27.9.12
Programm 4.27 C++: Generische Initialisierung von Reihungen vergleichbarer
Elemente ................................................................................................................43
Programm 4.28 C++: Generischer Testtreiber zu Reihungen vergleichbarer
Elemente ................................................................................................................44
Programm 4.29 C++: Testtreiber zu binärem Suchen in ganzzahligen Reihungen
.................................................................................................................................45
Programm 4.30 C++: Testtreiber zu Indexbereich mit maximaler Summe in
gleitpunktzahligen Reihungen ...............................................................................45
Programmeinheiten und -strukturen
Programm 5.1 C++: using-Direktive ......................................................................7
Programm 5.2 C++: Namensraum mit Aliasnamen ...............................................8
Programm 5.3 C#: using-Direktive ........................................................................9
Programm 5.4 Component Pascal: Kunden-Lieferanten-Module ........................14
Programm 5.5 C++: Kunden-Lieferanten-Module ..............................................16
Klassen und objektorientierte Konzepte
Typen und Datenkonzepte
Ablaufsteuerung und algorithmische Konzepte
Syntaktische und lexikalische Aspekte
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
xxiv
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Programmverzeichnis
27.9.12
Beispielverzeichnis
1
2
3
4
5
6
7
8
9
Einführung
Beispiel 1.1 Imperativ verbal: Fläche eines Rechtecks ...........................................6
Beispiel 1.2 Imperativ formal: Fläche eines Rechtecks ...........................................6
Beispiel 1.3 Funktional: Fläche eines Rechtecks ..................................................10
Beispiel 1.4 Miranda: Sieb des Eratosthenes .........................................................12
Beispiel 1.5 Prolog: Fläche eines Rechtecks .........................................................12
Beispiel 1.6 Regelbasiert: Fläche eines Rechtecks ................................................13
Einmal quer durch die Sprachen
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Reihungen und Abstraktionen
Programmeinheiten und -strukturen
Beispiel 5.1 Eiffel: Ace-Datei ..................................................................................6
Beispiel 5.2 Modulare Benutzungsbeziehung .......................................................14
Klassen und objektorientierte Konzepte
Typen und Datenkonzepte
Ablaufsteuerung und algorithmische Konzepte
Syntaktische und lexikalische Aspekte
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
xxv
xxvi
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Beispielverzeichnis
27.9.12
Leitlinienverzeichnis
1
2
3
4
5
6
7
8
9
Einführung
Einmal quer durch die Sprachen
Leitlinie 2.1 Vermeide Nebeneffekte .......................................................................4
Leitlinie 2.2 C++: Inkludiere nur bedingt ...............................................................7
Leitlinie 2.3 C++: Vermeide replizierte Konstanten ...............................................8
Leitlinie 2.4 Java: Vermeide replizierte Konstanten ...............................................9
Leitlinie 2.5 Bevorzuge Benutzung vor Mix-Ins ..................................................18
Leitlinie 2.6 C++: Vermeide unsichere C-Reihungen ...........................................20
Leitlinie 2.7 Ordne jedem Zweck eigene Größen zu ............................................21
Leitlinie 2.8 Vermeide leere catch-Blöcke ............................................................23
Leitlinie 2.9 Verwende Ausgabeparameter nur bei Prozeduren ............................25
Leitlinie 2.10 Component Pascal: Verberge Daten hinter Schnittstellen ..............27
Leitlinie 2.11 C*: Verberge Daten hinter Schnittstellen ........................................31
Leitlinie 2.12 C++: Lesbar statt kurz ....................................................................32
Leitlinie 2.13 C++: Denke an Redefinierbarkeit ...................................................33
Leitlinie 2.14 C#: Denke an Redefinierbarkeit .....................................................36
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Leitlinie 3.1 C*: Vermeide Sprunganweisungen ...................................................17
Leitlinie 3.2 Java: Bändige protected stark ...........................................................29
Leitlinie 3.3 Java: Bändige protected ....................................................................29
Reihungen und Abstraktionen
Leitlinie 4.1 C++: Benutze Vektoren statt Reihungen ..........................................11
Programmeinheiten und -strukturen
Leitlinie 5.1 C++: Vermeide using ..........................................................................7
Leitlinie 5.2 Java: Vermeide import ........................................................................8
Leitlinie 5.3 C#: Vermeide using .............................................................................9
Leitlinie 5.4 Module als ADSen ............................................................................14
Klassen und objektorientierte Konzepte
Leitlinie 6.1 C++: Vermeide Verdecken in abgeleiteten Klassen ..........................16
Leitlinie 6.2 C++: Vermeide Überladen virtueller Funktionen .............................17
Leitlinie 6.3 C++: Vereinbare Elementfunktionen virtuell ...................................17
Leitlinie 6.4 Java: Benutze @Override .................................................................18
Leitlinie 6.5 Java: Vermeide Verwirrendes ...........................................................18
Typen und Datenkonzepte
Leitlinie 7.1 C++: Vermeide Typkonversionen .......................................................9
Ablaufsteuerung und algorithmische Konzepte
Syntaktische und lexikalische Aspekte
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
xxvii
xxviii
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Leitlinienverzeichnis
27.9.12
Aufgabenverzeichnis
1
2
3
4
Einführung
Aufgabe 1.1 Zahlen interpretieren ........................................................................27
Einmal quer durch die Sprachen
Aufgabe 2.1 Hello World ......................................................................................12
Aufgabe 2.2 Abfrageorientierte Konversion .........................................................17
Aufgabe 2.3 Kommandoargumente ......................................................................26
Aufgabe 2.4 C++: Falsch getickt
.....................................................................34
Aufgabe 2.5 Java: Falsch getickt ...........................................................................35
Aufgabe 2.6 Code verbessern ................................................................................38
Aufgabe 2.7 Datum ...............................................................................................39
Aufgabe 2.8 Englischkenntnisse nachweisen ........................................................39
Aufgabe 2.9 Problem schematisch lösen ...............................................................39
Aufgabe 2.10 Chaostheorie – Feigenbaums Periodenverdopplung ......................39
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Aufgabe 3.1 C++: Größter gemeinsamer Teiler, fehlerhaft ..................................11
Aufgabe 3.2 Java: Größter gemeinsamer Teiler, fehlerhaft ..................................13
Aufgabe 3.3 C#: Größter gemeinsamer Teiler, fehlerhaft .....................................14
Aufgabe 3.4 ggT- und kgV-Rechner ......................................................................15
Aufgabe 3.5 ggT-Funktion iterativ variiert ...........................................................15
Aufgabe 3.6 Stellenzahl ........................................................................................16
Aufgabe 3.7 Sprunganweisungen? Nein danke! ...................................................16
Aufgabe 3.8 Eiffel: ggT, oo-endrekursiv mit Wertsemantik .................................21
Aufgabe 3.9 C#: ggT, oo-iterativ, Referenztyp .....................................................32
Aufgabe 3.10 C#: ggT, oo-rr, Werttyp ...................................................................33
Aufgabe 3.11 Ablaufverfolgung einer ggT-Berechnung .......................................34
Aufgabe 3.12 ggT- und kgV-Kunde ......................................................................35
Aufgabe 3.13 Zahlenpaarklassen, effizienter ........................................................35
Aufgabe 3.14 Stellenzahl, objektorientiert ............................................................35
Aufgabe 3.15 Ziffernquadratsummenfolge ...........................................................35
Aufgabe 3.16 Entwürfe 3.3.2, 3.3.4 ......................................................................60
Aufgabe 3.17 ggT- und kgV-Problemlöser ............................................................60
Aufgabe 3.18 Stellenzahl, konkretisierte Abstraktion ...........................................60
Reihungen und Abstraktionen
Aufgabe 4.1 Eiffel: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................8
Aufgabe 4.2 C++: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................14
Aufgabe 4.3 C++: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................16
Aufgabe 4.4 Java: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................19
Aufgabe 4.5 C#: Sortiertprüfung, binäres Suchen ................................................20
Aufgabe 4.6 Eiffel: Quicksort, rekursiv ................................................................24
Aufgabe 4.7 C++: Quicksort, rekursiv mit generischer Klasse ............................24
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
xxix
xxx
Aufgabenverzeichnis
5
6
7
8
9
Aufgabe 4.8 Java: Quicksort, rekursiv ..................................................................25
Aufgabe 4.9 C#: Quicksort, rekursiv ....................................................................25
Aufgabe 4.10 Eiffel: Testtreiber zu Indexbereich mit maximaler Summe ............29
Aufgabe 4.11 C++: Indexbereich mit maximaler Summe als Klassenfunktion ....30
Aufgabe 4.12 C++: Testtreiber zu Indexbereich mit maximaler Summe .............30
Aufgabe 4.13 Java: Indexbereich mit maximaler Summe ....................................31
Aufgabe 4.14 C#: Indexbereich mit maximaler Summe .......................................31
Aufgabe 4.15 C++: Abstrakte und konkrete Initialisierer und Testtreiber ............46
Aufgabe 4.16 C++: Testtreiber zu Klassenvarianten der Werkzeugkästen ...........46
Aufgabe 4.17 Java: Testtreiber mit wiederverwendbaren Teilen ..........................46
Aufgabe 4.18 C#: Testtreiber mit wiederverwendbaren Teilen ............................46
Programmeinheiten und -strukturen
Aufgabe 5.1 Java: Getrennte Kompilation ..............................................................4
Aufgabe 5.2 C#: Getrennte Kompilation ................................................................4
Klassen und objektorientierte Konzepte
Typen und Datenkonzepte
Ablaufsteuerung und algorithmische Konzepte
Syntaktische und lexikalische Aspekte
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1
Einführung
Aufgabe
Beispiel
1 nur
Bild
1 eine
1 (1)
Leitlinie Programm
1
1
Es gibt
Geschichte.
Tabelle 1
Karl Marx (1818 – 1883)
...aber es gibt unzählig viele Arten, sie zu erzählen.
Programmiersprache –
Programm –
Programmieren
Ekkehard Jost
Dieses Kapitel beleuchtet kurz verschiedene Aspekte von Programmiersprachen. In
weitem Sinn ist eine Programmiersprache (programming language) eine Notation
zur Darstellung von Softwaremodellen. In engem Sinn ist eine textuell notierte Programmiersprache eine formale Sprache zur Darstellung von Modellen, die Programme
heißen und sich auf Rechnern ausführen lassen. Ein Programm (program) ist eine
maschinell bearbeitbare Lösung eines informatischen Problems aus einem Anwendungsbereich. Programmieren bedeutet Programme erstellen. In weitem Sinn umfasst
Programmieren Tätigkeiten wie Analysieren, Spezifizieren, Entwerfen, Implementieren und Testen; in engem Sinn reduziert es sich auf das Codieren einer Problemlösung
in einer Programmiersprache.
Bild 1.1
Entwickler –
Programm –
Rechner
liest
versteht
programmiert
schreibt
Programmiersprache
Programm
Programm
Programm
läuft auf
speichert
bearbeitet
führt aus
Eine Programmiersprache ist intensional betrachtet eine Notation für Modelle, extensional betrachtet eine Gesamtheit von Programmen und Werkzeugen zum Bearbeiten
dieser Programme.
1.1
Klassifikation von Programmiersprachen
Tausende von Programmiersprachen wurden seit den 1940er Jahren entwickelt [Kin].
Sie werden teils nicht mehr, teils noch benutzt. Viele Sprachen werden weiter entwickelt. Auch wenn die Dynamik früherer Jahre nachgelassen hat, erscheinen immer wieder neue Sprachen. Wie soll sich der Informatiker im babylonischen Sprachenwirrwarr
zurechtfinden? Zur Orientierung ordnen wir die Menge der Programmiersprachen nach
den Kriterien (1) bis (8) und strukturieren sie durch Beziehungen zwischen Teilmengen. Nicht immer lässt sich eine Programmiersprache genau einem Aspekt eines Kriteriums zuordnen. Manche der folgenden Aspekte sind unscharf.
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 1 – Seite 1 von 34
1–2
Wo?
1 Einführung
(1)
Abstraktionsebene
Nah an der Maschine entspricht einer niederen Abstraktionsebene, nah an der
Anwendung einer hohen.
Maschinensprachen (machine language) sind durch Prozessortypen definiert.
Unter Maschinencode oder Objektcode versteht man die interne ausführbare
Darstellung eines Maschinenprogramms als Bitmuster.
Assemblersprachen (maschinenorientierte Sprache, assembly language) sind
symbolische, textuelle Darstellungen von Maschinensprachen. Ein Assemblerprogramm (Assemblercode) ist ein Programm in Assemblersprache.
Höhere Programmiersprachen (Hochsprache, high level language) haben
mächtigere Konstrukte als Maschinenbefehle, verbergen Details der Rechnerarchitektur, ermöglichen die Formulierung von Algorithmen oder allgemeiner
von Problemlösungen unabhängig von einem bestimmten Prozessortyp, und
orientieren sich an Bedürfnissen von Anwendungsbereichen. Ein Quellprogramm (Quellcode, Quelltext) ist ein Programm in Hochsprache.
Maschinen- und Assemblersprachen wird es so lange geben wie Rechner mit von-neumannscher Architektur. Jedoch sind Maschinensprachen für Menschen praktisch unlesbar, sodass sie nicht als Programmiermittel taugen. Assemblersprachen sind etwas besser lesbar, fordern vom Programmierer aber viele Detailkenntnisse des Prozessortyps
und bieten nur niedere, maschinenabhängige Abstraktionen. Daher scheiden sie heute
trotz ihrer Problemlösungsmächtigkeit – alles was die Maschine kann, kann die Assemblersprache – für die Anwendungsprogrammierung aus. Eingesetzt werden Assemblersprachen nur noch von Spezialisten zur Programmierung hardwarenaher Teile von
Betriebssystemen und z.B. von neuen Signalprozessoren, für die noch kein Hochsprachenübersetzer existiert. Dagegen wächst die Bedeutung von Hochsprachen.
Wozu?
(2)
Anwendungsbereich
Hochsprachen scheiden sich grob in vielseitig einsetzbare Mehrzwecksprachen
(general purpose, multipurpose) und problemorientierte Sprachen für spezielle
Anwendungsbereiche (special purpose, problem-oriented) wie:
Naturwissenschaft und Technik: komplizierte Berechnungen mit wenigen
numerischen Daten, Gleitpunktzahlen, Reihungen, Vektoren, Matrizen, Fallunterscheidungen und Wiederholungen (Fortran, Algol 60, APL)
Wirtschaft: einfache Berechnungen mit vielen Daten, Massendatenverarbeitung, Festpunktzahlen, Zeichen, Zeichenketten, Datensätze in Dateien und
Datenbanken, Berichte, Tabellenkalkulation (Cobol, Excel)
Künstliche Intelligenz (KI; artificial intelligence, AI): symbolische Berechnungen, verkettete Listen (Lisp, Prolog)
Lehre: Einfachheit, Lernbarkeit (Basic, Logo, Pascal)
Systemprogrammierung: Betriebssysteme, hardwarenahe Sprachkonzepte,
Effizienz, verteilte Systeme (Assemblersprachen, BCPL, Forth, C, Lis,
Modula, Go)
Kommandooberfläche von Betriebssystemen: schnelle Erweiterbarkeit
(Unix-Shell, MS-DOS-Shell)
Prozesssteuerung und Echtzeitdatenverarbeitung: Nebenläufigkeit (Forth,
Pearl, Ada, Chill)
Datenbankabfrage (SQL)
Skriptsprachen (Awk, Tcl/Tk, Perl, Python, Ruby)
Textformatierung und Seitenbeschreibung (Tex, Latex, PostScript, HTML,
CSS, XML, DTD, XML-Schema)
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.1 Klassifikation von Programmiersprachen
1–3
Grafik und virtuelle Realität (Logo, Cg, VRML, X3D)
Web (JavaScript, PHP, Flash, ActionScript, Curl, Ajax, XSLT, JSP, JSPSTL)
Die Aufzählung ließe sich fortsetzen. Gegenläufige Trends sind zu beobachten: Während einerseits Mehrzwecksprachen ihr Einsatzspektrum ausweiten, entstehen andererseits für neue Anwendungsbereiche neue kleine Spezialsprachen. Seit den 1960er Jahren decken PL/I, Simula 67, Algol 68, Pascal, C und Nachfolgersprachen die großen
Anwendungsbereiche Naturwissenschaft, Technik und Wirtschaft ab. C, Modula, Ada
und Nachfolger wie C++, Oberon und Component Pascal sind auch in die Systemprogrammierung vorgedrungen. Seit den 1990er Jahren sprießen aus den Nährböden Multimedia und Web vielfältige Sprachentwicklungen.
Wann?
(3)
Rolle im Softwareentwicklungsprozess
Analyse- und Entwurfssprachen (Petri-Netze, UML)
Spezifikationssprachen (Actor Model, Z, IDL, Eiffel, OCL, D, Spec#)
Implementationssprachen (Component Pascal, Eiffel, C++, Java, C#)
Ein Trend geht von konventionellen Implementationssprachen zu Sprachen, die frühe
Softwareentwicklungsphasen unterstützen. Beispiele sind die Implementationssprachen Eiffel, D und Spec# mit eingebauten Spezifikationskonstrukten. Die Grenze zwischen Entwurfs- und Spezifikationssprachen ist unscharf.
Was?
(4)
Programmierparadigma
imperativ (Component Pascal, Eiffel, C++, Java, C#)
funktional (Lisp, Scheme, ML, Miranda, Haskell, Gofer)
gemischt imperativ und funktional (CLOS, Dylan, Scala)
logikbasiert (Prolog)
regelbasiert (OPS5)
Den Begriff Programmierparadigma behandelt 1.2. Imperative Sprachen waren, sind
und bleiben voraussichtlich dominierend.
Wodurch?
(5)
Notation
textuell als Folge von Zeichen aus einem Zeichensatz (Component Pascal,
Eiffel, C++, Java, C#)
grafisch als Diagramm mit Kästchen und Pfeilen oder Graph mit Knoten und
Kanten (Petri-Netze)
gemischt textuell und grafisch (UML)
visuell (Visual Basic, Visual *)
Der Trend geht von rein textuellen Notationen zu grafischen Notationselementen. In
der objektorientierten Softwareentwicklung lassen sich Programmsysteme grafisch als
Klassendiagramme entwerfen, die Klassenschnittstellen textuell-grafisch spezifizieren
und aus attributierten Diagrammen unvollständige Quelltextschablonen erzeugen.
Eine visuelle Programmiersprache (visual programming language) ermöglicht auf
einfache Weise, grafische Benutzungsoberflächen (graphical user interface, GUI) zu
erzeugen. Oft gestaltet der Entwickler die Oberfläche mit einer Art Grafikeditor (GUIBuilder), ein Werkzeug erzeugt daraus Quelltextschablonen. Umgekehrt verfährt ein
BlackBox-Werkzeug, das aus Quelltext einfache Dialoge erzeugt.
Wie weit?
27.9.12
(6)
Ausführbarkeit
nicht maschinell ausführbare Modelle, Entwürfe und Spezifikationen
maschinell ausführbare Programme
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1–4
1 Einführung
Die Grenze fließt. Nicht ausführbare Modelle dienen oft als Zwischenprodukte, aus
denen manuell, rechnergestützt oder automatisch ausführbare Programme entstehen.
Programme von Implementationssprachen gelten als maschinell ausführbar.
Womit?
(7)
Wie?
(8)
Ausführendes Organ
Software: Interpretierer, virtuelle Maschine (Java, C#)
Hardware: Prozessor (Component Pascal, Eiffel, C++)
Implementationskonzept
Interpretation (Java, C#)
Kompilation (Component Pascal, Eiffel, C++, Java, C#)
Die Kriterien (6), (7) und (8) hängen zusammen. Die Unterscheidung bei (7) ist grob;
eine differenziertere Sichtweise erlaubt das Modell hierarchisch geschichteter virtueller
Rechner in Bild 1.2 [PrZ98] S. 81. Die Unterscheidung bei (8) ist nicht ausschließend,
da manche Sprachen sowohl kompiliert als auch interpretiert werden, und z.B. Java
und C# beide Techniken nutzen. Die Implementationskonzepte behandelt 1.4.2.3.
Bild 1.2
Hierarchie virtueller
Rechner
Anwendung
interpretiert Benutzereingaben
ist implementiert durch Hochsprache
Hochsprache
interpretiert Quellprogrammtext
ist implementiert durch Übersetzer und Laufzeitsystem
Betriebssystem
interpretiert Systemaufrufe
ist implementiert durch Maschinenbefehle
Firmware
interpretiert Maschinenbefehle
ist implementiert durch Mikrocodebefehle
Hardware
interpretiert Mikrocodebefehle
ist implementiert durch physische Geräte
1.2
Programmierparadigmen
Paradigma
Ein Paradigma1 ist nach Thomas S. Kuhn eine allgemeine wissenschaftliche Leitidee,
auf deren Basis eine Wissenschaftlergemeinschaft langfristig arbeitet.2 Paradigmen
gehen nicht stetig ineinander über, sondern stehen unvergleichbar gegeneinander. Gelegentlich findet ein Paradigmenwechsel statt: Ein vorherrschendes Paradigma wird in
einer wissenschaftlichen Revolution durch ein neues Paradigma abgelöst.
Programmierparadigma
Den Paradigmenbegriff in die Programmierwelt zu übernehmen ist problematisch, aber
nicht unnütz. Die freie Enzyklopädie Wikipedia übertreibt mit einer dynamischen Liste
von 15 oder 12 Programmierparadigmen, da diese nicht unvergleichbar gegeneinander
stehen, sondern sich vielfach ergänzen.3 Deshalb sei hier eingeschränkt:
1
Griechisch: Beispiel, Vorbild, Muster, Abgrenzung.
2
Thomas S. Kuhn (1922 – 1996), britischer Wissenschaftstheoretiker, lieferte mit The Structure of Scientific Revolutions (1962) ein einflussreiches Werk.
3
http://de.wikipedia.org/wiki/Programmierparadigma (Zugriffe 2005-08-15, 2012-03-07).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.2 Programmierparadigmen
1–5
Ein Programmierparadigma (programming paradigm) umfasst die zentralen Prinzipien, Konzepte und Begriffe, die nur einer bestimmten Programmiertechnik zugrunde
liegen. Verschiedene Programmierparadigmen unterscheiden sich wesentlich in ihren
Prinzipien, Konzepten und Begriffen. Im Unterschied zu kuhnschen Paradigmen lösen
sich Programmierparadigmen nicht durch Paradigmenwechsel ab, sondern koexistieren
in ihren Anwendungsbereichen seit langem bis auf Weiteres. Zudem sind allgemeine
Prinzipien in mehreren Paradigmen zu finden, etwa das Teile-und-herrsche-Prinzip
(engl. divide and conquer, lat. divide et impera), nach dem man ein Problem in Teilprobleme zerlegt, die Teilprobleme löst und die Teilproblemlösungen zu einer Problemlösung zusammensetzt.
Die wichtigsten Programmierparadigmen, nämlich
das imperative,
das funktionale,
das logikbasierte, und
das regelbasierte
stellt das Folgende kurz zur Allgemeinbildung vor. Überblicke zu Programmierparadigmen geben viele Lehrbücher, etwa [ApL92], [Lou93], [Seb06], [Set97]. Zahlreiche
Arten der Programmierung sind durch Adjektive und Subjektive charakterisiert:
adaptive P., agentenorientierte P., datenstromorientierte P., defensive P., egolose P.,
evolutionäre P., extreme P., generative P., genetische P., intentionale P., konkatenative P., literarische P., sprachorientierte P., subjektorientierte P.
Constraint-P., Paarp.
Teils sind es Programmierstile, die sich einem Programmierparadigma unterordnen lassen, teils Programmierkonzepte für bestimmte Anwendungsbereiche, teils befassen sie
sich mehr mit der Organisation des Programmierprozesses als dem Programm.
Programmiersprachen unterstützen meist ein bestimmtes Programmierparadigma, oft
mehrere Paradigmen durch sprachliche Ausdrucksmittel – Sprachkonstrukte. Entsprechend spricht man von imperativen, funktionalen, logikbasierten und regelbasierten
Programmiersprachen.
1.2.1
Imperatives Programmierparadigma
Prinzip
Das imperative Programmierparadigma widerspiegelt direkt die praktisch realisierte
von-neumannsche Rechnerarchitektur, der in der theoretischen Informatik die TuringMaschine als formales Modell für Berechnungen entspricht.1, 2 Zu den Prinzipien des
imperativen Programmierparadigmas gehört, Programme als Befehlsfolgen aufzufassen, die von Automaten ausgeführt werden. Imperative Programme bestehen aus
Daten (data) mit veränderbaren Zuständen (state), wobei die aktuellen Zustände in
einem Speicher abgelegt sind;
Befehlen (instruction), die von Prozessoren ausgeführt werden, wobei die Ausführung (execution) eines Befehls einen Effekt bewirkt, nämlich Zustände von Daten
verändert.
1
John von Neumann (1903 – 1957), genialer ungarisch-US-amerikanischer Mathematiker,
begründete die Spieltheorie, trug zu vielen Gebieten wie Mengentheorie, Quantenmechanik,
Differenzialgleichungen, Rechnerarchitektur bei.
2
Alan Turing (1912 – 1954), britischer Mathematiker und Logiker, leistete Herausragendes für
die theoretische Informatik und die Kryptanalyse; trug im Zweiten Weltkrieg wesentlich zur
Entschlüsselung des deutschen Enigma-Codes und damit zum Sieg der Alliierten über das Naziregime bei; wurde durch Verurteilung zu zwei Jahren Gefängnis wegen homosexueller Handlungen in den Suizid getrieben.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1–6
Beispiel 1.1
Imperativ verbal:
Fläche eines
Rechtecks
Konzept und Begriff
1 Einführung
Daten: Drei Zahlen namens width, height und area.
Befehle: Schreibe den Wert 2 in die Zahl width.
Schreibe den Wert 3 in die Zahl height.
Schreibe das Produkt der Werte von width und height in die Zahl area.
Zu seinen Konzepten gehört, Daten als Ansammlungen einzelner Variablen zu vereinbaren; Variablen durch Zuweisungen (assignment) an Werte (value) zu binden; Befehle
linear angeordnet aufzuschreiben und sequenziell nacheinander auszuführen; Befehlsarten zu unterteilen in solche, die auf Variablen lesend oder schreibend zugreifen, und
solche, die den Ablauf einer Befehlsfolge steuern. Neben anderen Bezeichnungen für
Befehl wie Instruktion, Kommando, Anweisung gehören zu seinen Begriffen z.B.
Datenbefehl, Lesezugriff, Schreibzugriff, Sprungbefehl, Steueranweisung.
Werte können Typen zugeordnet sein, die Operationen auf diesen Werten definieren.
Variablen können statisch oder dynamisch typisiert, d.h. an Typen gebunden sein. Die
Anwendung von Operationen auf Variablen führt zu Ausdrücken (expression).
Da im imperativen Programmierparadigma ein Programm einen Algorithmus oder
mehrere zusammenwirkende Algorithmen beschreibt, heißt es auch algorithmisches
Programmierparadigma. Da sich ein Programm auch als Folge von Zuständen
beschreiben lässt, heißt es auch zustandsorientiertes Programmierparadigma. Das
Programmieren mit Zusicherungen (assertion) betont diese Sicht.
Bild 1.3
Zustandsübergangsdiagramm
Beispiel 1.2
Imperativ formal:
Fläche eines
Rechtecks
Daten
Vorzustand
Daten
Nachzustand
Befehl
Daten: width, height, area : FLOAT
Befehle: 2 → width
3 → height
width * height → area
Zustände:
width = 2
height = 3
area = width * height
area = 6
Im Beispiel 1.2 ist „→“ das Zuweisungssymbol, wobei die Zuweisung in der in diesem
Kulturkreis üblichen Leserichtung von links nach rechts zu lesen ist; „=“ ist das jahrhundertealte mathematische Gleichheitssymbol.
Stil
Strukturiert
Die folgenden Programmierstile (programming style) sind spezielle Ausprägungen
des imperativen Paradigmas. Obwohl manchmal als eigenständige Paradigmen
bezeichnet, stehen sie nicht gegeneinander, sondern bauen aufeinander auf.
Strukturiertes Programmieren (structured programming) bedeutet, Algorithmen
mit Anweisungsfolgen, Auswahl- und Wiederholungsanweisungen und Prozeduren
zu formulieren. Jedes Konstrukt zur Ablaufsteuerung hat genau je einen Ein- und
Ausgang. Das ist mehr als auf Sprunganweisungen (goto, if ... goto) zu verzichten!
Bild 1.4
Struktogramm
Strukturiertes Programmieren entstand in den 1960er Jahren zusammen mit der
Top-Down-Algorithmenentwurfsmethode schrittweises Verfeinern (stepwise refine-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.2 Programmierparadigmen
Prozedural
Bild 1.5
Prozeduren mit
Benutzung und
Schachtelung
1–7
ment) und Verifikationsmethoden wie der axiomatischen Semantik, um Probleme
undurchschaubaren Spaghetticodes zu bewältigen.
Prozedurales Programmieren (procedural programming) bedeutet, ein Problem in
Teilprobleme zu zerlegen, diese durch Prozeduren und das Problem durch Zusammenwirken der Prozeduren zu lösen. Prozeduren stehen in Benutzungs- oder Aufrufbeziehungen zueinander. Eine Prozedur ist eine Abstraktion von Anweisungen. Sie
bietet eine Schnittstelle (interface) aus einem Namen und einer Parameterliste, hinter der sie eine Implementation aus lokalen Daten und Anweisungen verbirgt (kapselt und schützt). Prozeduraufrufe (procedure call) bewirken Effekte. Textuelle
Schachtelung (nesting) von Prozeduren schränkt ihre Sichtbarkeit ein. Prozedurales
Programmieren entstand in den 1950er Jahren zwecks Wiederverwendung von
Codestücken. Während die Algorithmen prozedural strukturiert sind, fehlt eine entsprechende Strukturierung global vereinbarter und überall sichtbarer Daten.
Name (Parameterliste)
P
Q
lokale Daten
Anweisungen
call Q
call R
R
S
T
U
globale
Daten
call S
call S
Modular
Bild 1.6
Module mit Benutzung
und Schachtelung
Modulares Programmieren (modular programming) bedeutet, ein Problem in
Teilprobleme zu zerlegen, diese durch Module und das Problem durch Zusammenwirken der Module zu lösen. Module stehen in Benutzungs- oder Kunde-LieferantBeziehungen zueinander. Ein Modul hat einen Namen und bietet eine Schnittstelle,
die vorzüglich aus Prozeduren besteht, synonym oft Operationen genannt. Modulares Programmieren kennt keine global vereinbarten, sondern nur gekapselte Daten,
die aber nicht geschützt sein müssen. Schachtelung von Modulen kommt vor, ist
aber weniger wichtig als die Benutzungsbeziehung. Modulares Programmieren entstand in den 1970er Jahren, um Probleme großer Softwaresysteme zu bewältigen.
Name
gekapselte
Daten
Operation
A
B
call B.P
call C.P
C
D
E
F
Operation
ADS
27.9.12
Programmieren mit abstrakten Datenstrukturen: Ein Modul ist vorzüglich eine
abstrakte Datenstruktur (abstract data structure, ADS), es verbirgt hinter seiner
Schnittstelle eine Implementation aus gekapselten und geschützten konkreten Daten
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1–8
ADT
1 Einführung
und Algorithmen der Operationen, die auf die Daten zugreifen. Eine ADS ist ausschließlich durch ihr extern beobachtbares Verhalten definiert, spezifiziert durch die
Menge anwendbarer Operationen, wobei jede Operation durch ihre Signatur und
Aufrufkonvention und ihren Effekt beschrieben ist. Programmieren mit ADSen
kennt keine überall sichtbaren Daten, Zustände von ADSen sind nur durch Operationsaufrufe abfragbar.
Programmieren mit abstrakten Datentypen bedeutet, Probleme mit Exemplaren
abstrakter Datentypen zu lösen, die in Benutzungsbeziehungen zueinander stehen.
Ein abstrakter Datentyp (abstract data type, ADT) ist eine zu einem Typ abstrahierte ADS. Die Typeigenschaft bedeutet, dass sich von einem ADT beliebig viele
Exemplare bilden lassen, die alle ADSen mit denselben Operationen und demselben
Verhalten, aber jeweils eigenen Zuständen und internen Daten darstellen. Programmieren mit abstrakten Datentypen entstand in den 1970er Jahren zusammen mit
Methoden zur Spezifikation und Verifikation von Programmkomponenten.
Bild 1.7
Abstrakter Datentyp
mit Exemplaren
Typ
Exemplar 1
Objektorientiert
Exemplar 2
Exemplar 3
Objektorientiertes Programmieren (object-oriented programming) bedeutet, Probleme mit Objekte genannten Exemplaren von Klassen zu lösen. Klassen sind mehr
oder weniger implementierte ADTen, die in Benutzungs- und Vererbungsbeziehungen zueinander stehen können. Ein zentrales Konzept ist Polymorphie und dynamisches Binden: Namen können sich auf dynamisch erzeugte und gebundene Objekte
verschiedener Klassen beziehen. Wie bei Modulen ist Schachtelung von Klassen
eher marginal interessant. Objektorientiertes Programmieren entstand in den 1960er
Jahren für Simulationsanwendungen und in den 1970er Jahren zusammen mit grafischen Bildschirmen und Arbeitsplatzrechnern.
Bild 1.8
Klassen mit Benutzung
und Vererbung
Oberklasse
Kunde
Klasse
Lieferant
Unterklasse
Komponentenorientiert
Komponentenorientiertes Programmieren (component-oriented programming)
ermöglicht, unabhängig voneinander entwickelte wiederverwendbare Pro-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.2 Programmierparadigmen
1–9
grammeinheiten bei Bedarf dynamisch zu binden und zu laden. Komponenten sind
oft Ansammlungen von Modulen und Klassen. Komponentenorientiertes Programmieren entstand in den 1990er Jahren, um Softwaresysteme flexibler zu gestalten.
Bild 1.9
Komponenten
your_component.dll
her_component.dll
dynamisch gebunden
dynamisch gebunden
my_prog.exe
statisch gebunden
Generisch
Generisches Programmieren (generic programming) ermöglicht, Programmeinheiten, Algorithmen und Datenstrukturen so verallgemeinert zu definieren, dass sie
für verschiedene Typen verwendbar sind. Die Definition einer generischen Programmeinheit ist mit formalen generischen Parametern versehen, die bei jeder Verwendung der Programmeinheit durch aktuelle konkrete Typen ersetzt werden.
Generisches Programmieren entstand in den 1980er Jahren zwecks Wiederverwendung von Programmeinheiten.
Bild 1.10
Generische Einheit mit
Konkretisierungen
Einheit [Typ]
Einheit [FLOAT]
Nebenläufig
Einheit [SET]
Einheit [LIST]
Sprache
Nebenläufiges Programmieren (concurrent programming) bedeutet, Probleme
durch Strukturieren in mehrere sequenzielle Abläufe zu lösen. Oft sind Prozesse die
sequenziellen Ablaufeinheiten. Jeder Prozess löst ein Teilproblem. Die Prozesse
konkurrieren um benötigte Betriebsmittel; zur Problemlösung kooperieren sie, synchronisieren sich und kommunizieren miteinander. Nebenläufiges Programmieren
entstand in den 1960er Jahren im Bereich Betriebssysteme.
Aspektorientiertes Programmieren (aspect-oriented programming) versucht,
Aspekte voneinander zu trennen, die quer zu konventionellen Struktureinheiten
(Prozedur, Modul, Klasse) auftreten. Beispiele dafür sind Fehler- und Ausnahmebehandlung, Protokollierung und Persistenz. Ziel ist, solche bisher über mehrere
Struktureinheiten verstreuten Aspekte besser zu kapseln. Aspektorientiertes Programmieren entsteht seit den 1990er Jahren, um Wartbarkeit und Wiederverwendbarkeit von Softwaresystemen zu erhöhen.
Einige wichtige imperative Programmiersprachen sind in 1.6 und Bild 1.13 vorgestellt.
1.2.2
Funktionales Programmierparadigma
Prinzip
Das funktionale Programmierparadigma beruht auf Funktionen im mathematischen
Sinn, dem Lambdakalkül und der Rekursionstheorie. Mathematische Funktionen sind
Abbildungen der Form
Aspektorientiert
f : M → N, x |→ f(x) = y
mit einem Definitionsbereich M, einem Wertebereich N und einer Abbildungsvorschrift
x |→ f(x), die oft durch einen Ausdruck darstellbar ist. Zu den Prinzipien des funktio-
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 10
1 Einführung
nalen Programmierparadigmas gehört, Programme als Funktionen aufzufassen und aus
Funktionen aufzubauen, wobei
Funktionsdefinitionen Funktionen definieren und
Funktionsanwendungen Funktionsdefinitionen anwenden.
Man spricht auch vom applikativen Programmierparadigma, da man Probleme
durch Funktionsanwendungen löst.
Beispiel 1.3
Funktional: Fläche
eines Rechtecks
Konzept und Begriff
Funktionsdefinition:
area : +2 → +, area (width, height) = width * height.
Funktionsanwendung: area (2, 3).
Wert:
6.
Zu seinen Konzepten gehört: Eine Funktion ist eine Abstraktion eines Ausdrucks. Die
Auswertung eines Ausdrucks liefert einen Wert. Der Wert eines Ausdrucks hängt nur
von den Werten seiner Teilausdrücke ab, sofern vorhanden.
Ein funktionales Programm besteht aus einer Ansammlung von Funktionsdefinitionen.
Ein Programmablauf entsteht durch eine Funktionsanwendung, in der Anfangswerte
angegeben werden.
Die Argumente der Funktionen heißen Parameter. In Funktionsdefinitionen erscheinen
formale Parameter als Namen, in Funktionsanwendungen erscheinen aktuelle Parameter als Ausdrücke. Eine Funktionsdefinition definiert eine Funktion durch einen
Namen, eine Formalparameterliste und einen Ausdruck. Eine Funktionsanwendung
oder ein Funktionsaufruf ist ein Ausdruck, zu dessen Auswertung die aktuellen Parameter ausgewertet und ihre Werte in den definierenden Ausdruck eingesetzt werden,
dieser ausgewertet und der erhaltene Wert als Ergebniswert in die Anwendungsstelle
eingesetzt, m.a.W. als Rückgabewert an die Aufrufstelle zurückgegeben wird. Parameter können statisch oder dynamisch typisiert sein.
Ein Ausdruck setzt sich aus Funktionsanwendungen zusammen. Gewisse Grundfunktionen sind vorgegeben. Funktionen sind Werte erster Klasse, d.h. sie können als Parameter, Ergebnisse und Teil von Datenstrukturen auftreten. Damit lassen sich Funktionen zu höheren Funktionen komponieren und Operationen auf Datenstrukturen
definieren.
Rekursion
Ein wesentliches Konzept des funktionalen Programmierparadigmas ist Rekursion in
Form rekursiver Funktionsanwendungen. Bei direkter Rekursion enthält der definierende Ausdruck eine Anwendung der definierten Funktion. Bei indirekter Rekursion
mit den Funktionen f1,.., fn enthält der definierende Ausdruck von fi eine Anwendung
von fi mod n + 1.
Bezugstransparenz
Ein weiteres wichtiges Konzept ist Bezugstransparenz (referential transparency), was
allgemein fordert, dass Zugriffe unabhängig von Implementationen sind. Hier bedeutet
es, dass das Ergebnis einer Funktionsanwendung nur von der Funktion und den aktuellen Parametern abhängt, nicht von der Reihenfolge ihrer Auswertung, der Stelle oder
dem Zeitpunkt der Anwendung.
Sprache
Zu den funktionalen Programmiersprachen zählt als erste Lisp (List Processor,
McCarthy, 1960); spätere Entwicklungen sind Scheme (Sussman, Steele, 1975), Common Lisp (Steele, 1984), CLOS (Common Lisp Object System, Bobrow, 1988), ML
(MetaLanguage, Milner, Tofte, Harper, 1990), Miranda (Turner, 1986), Haskell
(Hudak, Fasel, 1992), Gofer (Jones, 1991). Anwendungsbereiche sind vor allem die
Künstliche Intelligenz und die Wissensverarbeitung. Beeinflusst von CLOS entstehen
Sprachen wie Dylan (Apple Computer, Anfang 1990er Jahre) und Scala (Odersky,
2001), die das funktionale Paradigma mit Objektorientierung verbinden.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.2 Programmierparadigmen
Tabelle 1.1
Funktional: Fläche
eines Rechtecks
Funktional versus
imperativ
Programmiersprache
1 – 11
Funktionsdefinition
Funktionsanwendung
Lisp
(area (LAMBDA (width height) width * height))
(area 2 3)
Scheme
(define (area width height) (* width height))
(area 2 3)
ML
fun area (width, height : real * real) : real =
width * height;
area (2, 3)
Miranda
area (width, height) = width * height
area (2, 3)
Worin unterscheidet sich das funktionale vom imperativen Programmierparadigma?
Funktionen trennen klar zwischen hereinkommenden Werten – Parametern – und
hinausgehenden Werten – Ergebnissen. Dagegen können bei imperativen Prozeduren, die auf globale Daten zugreifen oder Referenzparameter benutzen, dieselben
Daten als Ein- und Ausgabegrößen auftreten.
Funktionen erhalten alle Werte durch Parameter. Es gibt keine Zustände, keine Variablen, keine Zuweisungen. Daher kann kein Funktionsergebnis von einem Zustand
abhängen. Funktionen können keine Nebeneffekte bewirken. Dagegen sind
Zustände beim imperativen Programmieren wesentlich. Funktionen in imperativen
Sprachen können Nebeneffekte (side-effect) bewirken, d.h. sie können nicht nur ein
Ergebnis zurückgeben, sondern auch Zustände globaler Variablen verändern.
Entsprechendes gilt für Ausdrücke: Im funktionalen Paradigma sind Ausdrücke
stets nebeneffektfrei, im imperativen Paradigma können sie nebeneffektbehaftet
sein.
Nebeneffektfreiheit ist ein Aspekt der Bezugstransparenz: Funktionen sind von
Natur aus nebeneffektfrei. Oft sind sie auch bezugstransparent insofern, als ihre
Ergebnisse nicht von Anwendungsstellen und Auswertungsreihenfolgen der Parameter abhängen. Dagegen können beim imperativen Programmieren nebeneffektbehaftete Ausdrücke die Bezugstransparenz von Routinen, d.h. Prozeduren und
Funktionen, stören.
Es gibt keine Anweisungen, keine Steueranweisungen, keine Schleifen. Bedingte
Ausdrücke und rekursive Funktionsanwendungen erledigen die Berechnungen.
Speicher wird implizit verwaltet, unbenutzte Speicherbereiche werden automatisch
freigegeben. Dagegen muss im imperativen Programmierparadigma der Programmierer die Speicherarten globaler Speicher, Keller und Halde kennen und Speicherbereiche durch Vereinbaren statischer oder kellerdynamischer Variablen und Erzeugen und ggf. Freigeben dynamischer Variablen explizit selbst verwalten.
Wer das funktionale Programmierparadigma kennt, wird dessen Vorteile beim imperativen Programmieren nutzen, sorgfältig mit Funktionen arbeiten, nebeneffektfreie
Funktionen bevorzugen und nebeneffektbehaftete Funktionen vermeiden.
Problemlösungsmächtigkeit
Der Church-Turing-These zufolge lassen sich im Lambdakalkül und mit einer TuringMaschine dieselben Probleme lösen.1 Daher sind rein funktionale Programmiersprachen Turing-vollständig, d.h. sie können alle Berechnungen beschreiben, die eine
Turing-Maschine ausführen kann. Damit können funktionale Sprachen alle mit imperativen Sprachen lösbaren Probleme lösen, allerdings – und hier liegt ihr Nachteil – oft
weniger effizient.
1
Alonzo Church (1903 – 1995), US-amerikanischer Mathematiker, trug wesentlich zur theoretischen Informatik bei, entwickelte den Lambdakalkül, entdeckte die Unentscheidbarkeit formaler Probleme. Nach der Church-Turing-These sind viele Berechnungsmodelle äquivalent.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 12
Ausdrucksstärke
1 Einführung
Ein Vorteil funktionaler Sprachen ist dagegen ihre Einfachheit und Ausdrucksstärke.
Viele Problemlösungen erscheinen mit funktionalen Sprachen kompakter und prägnanter als mit imperativen Sprachen. Während etwa für das Sieb des Eratosthenes zur
Bestimmung der Primzahlen eine imperative Lösung minimal rund 20 Anweisungen,
darunter einige Schleifen und eine Fallunterscheidung benötigt, genügen einer funktionalen Lösung zwei Zeilen.1
Beispiel 1.4
Miranda: Sieb des
Eratosthenes
sieve (h : t) = h : sieve [ n | n <- t; n mod h ~= 0 ]
primes = sieve [ 2 .. ]
1.2.3
Logikbasiertes Programmierparadigma
Prinzip
Das logikbasierte (prädikative, wissensbasierte) Programmierparadigma beruht
auf der Prädikatenlogik. Zu seinen Prinzipien gehört, Programmabläufe als Beweise in
einem System logischer Aussagen aufzufassen. Man beschreibt ein Problem durch
Konzept und Begriff
Sprache
eine Menge von Eigenschaften (predicate) in Form von
Fakten (fact), d.h. gültigen Prädikaten, und
Regeln (rule), d.h. Implikationen von Fakten, um neue Fakten zu gewinnen, und
Abfragen (goal, query), die bestimmte Eigenschaften prüfen und mit yes oder no
antworten, ggf. auch mit den für yes zutreffenden Belegungen der Variablen in der
Abfrage.
Zu seinen Konzepten gehört: Ein Regelinterpretierer versucht, aus den Fakten und
Regeln Antworten auf die Abfragen zu gewinnen. Dabei spielen Horn-Verfahren und
Horn-Klauseln eine wichtige Rolle.
Die bekannteste logikbasierte Programmiersprache ist Prolog (programmation en
logique, Colmerauer, Kowalski, Roussel, 1972). Anwendungsbereiche sind die Künstliche Intelligenz und die Wissensverarbeitung.
Beispiel 1.5
Prolog: Fläche eines
Rechtecks
Regeln:
valid (width, height) :- (width > 0), (height > 0).
(area = width * height) :- valid (width, height).
Fakten:
(width = 2).
(height = 3).
Abfrage:
Antwort:
Abfrage:
Antwort:
?- (area = 5).
no
?- (area = x).
x=6
Das Symbol „:-“ entspricht dem Implikationspfeil „⇐“, wobei der rechte Ausdruck den
linken impliziert. In der zweiten Abfrage ist x eine Variable, für die der Regelinterpretierer einen passenden Wert findet.
1.2.4
Regelbasiertes Programmierparadigma
Prinzip
Das regelbasierte Programmierparadigma kombiniert logische und imperative Elemente. Zu seinen Prinzipien gehört, Programme als Zustände und Zustandsübergangsregeln aufzufassen. Man beschreibt ein Problem durch
einen Startzustand und einen Zielzustand und
eine Menge von Wenn-Dann-Regeln der Form „Wenn Bedingung gilt, dann Aktion
ausführen“.
1
Eratosthenes von Kyrene (ca. 282 – 202 v.u.Z.), griechischer Gelehrter in Alexandria, arbeitete zur Philologie, Grammatik, Literaturgeschichte, Mathematik, Chronologie, Astronomie und
Geographie.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.2 Programmierparadigmen
Konzept und Begriff
1 – 13
Zu seinen Konzepten gehört: Der Problembereich wird durch Daten in Form von Variablen modelliert. Zustände setzen sich aus Bedingungen an Variablen zusammen. Aktionen sind Zustandsveränderungen wie Merken und Streichen von Bedingungen.
Beispiel 1.6
Regelbasiert: Fläche
eines Rechtecks
Startzustand:
width = 0, height = 0, area = width * height,
Ziel (width > 0, height > 0)
Wenn-Dann-Regeln: Wenn width = 0 und Ziel (width > 0, height > 0) dann
merke (width = 2, Ziel (height > 0))
streiche (width = 0, Ziel (width > 0, height > 0))
Wenn height = 0 und Ziel (width > 0, height > 0) dann
merke (height = 3, Ziel (width > 0))
streiche (height = 0, Ziel (width > 0, height > 0))
Wenn width = 0 und Ziel (width > 0) dann
merke (width = 2)
streiche (width = 0, Ziel (width > 0))
Wenn height = 0 und Ziel (height > 0) dann
merke (height = 3)
streiche (height = 0, Ziel (height > 0))
Sprache
Regelbasierte Programmiersprachen treten in vielen Variationen auf; ein Beispiel ist
OPS5. Anwendungsbereiche sind die Wissensverarbeitung und die Mathematik.
Regelbasierte Programmiersprachen hängen mit Produktionensystemen zusammen.
„Ein Produktionensystem besteht aus einer globalen Datenbasis, die die Fakten des
Problems enthält, einer Regelbasis mit den auf der Datenbasis operierenden (Produktions)Regeln und einem Regelinterpreter (inference engine), der die Systemaktivitäten
mittels eines Erkenne-Agiere-Zyklus (recognize-act cycle) steuert. Produktionensysteme eignen sich zur Lösung von Problemen, bei denen das zu programmierende Wissen in natürlicher Weise in Regelform vorkommt oder bei denen die Programmkontrolle sehr komplex ist.“1
Für mathematische Anwendungen ist es oft zweckmäßig, Programme als Anwendung
von Umformungsregeln zu formulieren. Die Regeln werden mit Mustererkennungsalgorithmen (pattern matching) angewandt. So ist etwa der Kern des Computeralgebrasystems Mathematica aufgebaut.2
1.2.5
Fazit
Imperativ
Im imperativen Programmierparadigma muss der Programmierer das „Wie?“ einer Problemlösung festlegen, indem er einen vollständigen Lösungsweg in Form eines Algorithmus entwirft und in ein Programm umsetzt. Das Programm läuft i.d.R. deterministisch ab.
Deklarativ
Das funktionale Programmierparadigma kennt keine Algorithmen, keine Variablen,
keine Anweisungen. Funktionsdefinitionen sind deklarativ, nicht imperativ. Aber das
„Wie?“ einer Problemlösung ist festgelegt, da Ausdrücke i.d.R. deterministisch ausgewertet werden.
1
F. J. Radermacher, Th. Kämpke, Th. Rose, K. Tochtermann, T. Richter: „Management von
nicht-explizitem Wissen: Noch mehr von der Natur lernen“ Abschlussbericht Teil 2 Wissensmanagement: Ansätze und Erfahrungen in der Umsetzung. Hrsg: Forschungsinstitut für anwendungsorientierte Wissensverarbeitung (FAW) Ulm, erstellt im Auftrag des Bundesministeriums
für Bildung und Forschung (bmb+f) (März 2001) S. 65 von 121 S.; http://www.faw-neu-ulm.de/
sites/default/files/BMBF_Studie_Teil_2.pdf (Zugriff 2012-03-09).
2
http://wwwmath1.uni-muenster.de/u/mathinfo/kommvor/ws9899/cleantex/Kommentare/
node20.html (Zugriff 2005-08-25).
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 14
1 Einführung
Deskriptiv
Im Unterschied dazu muss der Programmierer im logik- und im regelbasierten Programmierparadigma nur das „Was?“ einer Problemlösung beschreiben, die Eigenschaften der Lösung als Ziel. Eigenschaften, Fakten, Regeln sind deskriptiv.
Nichtdeterministisch
Gemeinsames Merkmal ist der Nichtdeterminismus: Ein Regelinterpretierer kann die
Regeln in beliebiger, nichtdeterministischer Reihenfolge anwenden. Die Programme
selbst legen nicht fest, ob und wie das Ergebnis zu berechnen oder das Ziel zu erreichen
ist. Allerdings arbeiten viele Regelinterpretierer praktisch mit deterministischen Strategien, nach denen sie die Regeln anwenden.
Die verschiedenen Programmierparadigmen entsprechen damit verschiedenen Abstraktionsebenen, die von der niederen Abstraktionsebene des imperativen Paradigmas über
die mittlere Ebene des funktionalen, deklarativen Paradigmas zur hohen Ebene der deskriptiven Paradigmen reichen.
1.3
Abstraktionsebenen in imperativen Sprachen
Abstraktion
Abstraktion ist ein Kernthema der Informatik. Schon der frühgeschichtliche Mensch
abstrahierte von den ihn umgebenden Dingen und klassifizierte seine Begriffe. Als der
aus dem Alten Testament bekannte Noah sein Schiff füllte, wusste er Lebewesen in
Pflanzen und Tiere zu unterscheiden und kannte als Tiere Ziegen, Schweine, Kamele
und Elefanten. Griechische Philosophen wie Heraklit, Platon, Aristoteles befassten sich
mit Abstraktion.
Klassifikation und
Bestandteilbeziehung
Heute bieten objektorientierte Programmiersprachen Vererbung als Mittel, um in die
Software abgebildete Begriffe zu klassifizieren. Orthogonal zur Klassifikation steht die
ebenso wichtige Bestandteilbeziehung. Das Zerlegen eines Ganzen in seine Teile ist
eine Form des Konkretisierens, das Zusammenfassen einzelner Dinge zu einem einheitlichen Ganzen eine Form des Abstrahierens. Objektorientiertes Modellieren arbeitet
mit Aggregation und Komposition, objektorientierte Programmiersprachen bilden
Bestandteil- auf Benutzungsbeziehungen ab.
Imperative
Programmiersprache
Das Folgende bezieht sich auf das imperative Programmierparadigma. Zu diesem
Abschnitt siehe auch [War02] Chapter 23. Die Entwicklung der Programmiersprachen
zeigt einen Trend von niederen zu immer höheren Abstraktionsebenen. Ausgehend von
Sprachkonzepten, die Merkmale der Maschine widerspiegeln, sucht man stets nach
abstrakteren Sprachkonzepten, die oft wiederkehrende Lösungsmuster extrahieren und
es ermöglichen, Anwendungsprobleme noch besser angemessen auszudrücken.
(1)
(2)
(3)
Von-neumannsche Rechnerarchitektur
Speicher als Reihung von Speicherzellen für Bytes
Register
Maschinensprache mit Befehlen zum Lesen und Schreiben von Speicherzellen, Manipulieren von Registerinhalten und Steuern des Programmablaufs
Maschinennahe Ebene
Variablen mit Namen statt Adressen
Zuweisung (Wert → Variable)
Marken vor Anweisungen (Marke : Anweisung)
Bedingte Sprunganweisungen (if Bedingung goto Marke)
Ausdrucksstrukturierung
Beliebig komplexe, rekursiv aufgebaute Ausdrücke zur Berechnung von
Werten
Bezugstransparenz
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.3 Abstraktionsebenen in imperativen Sprachen
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
27.9.12
1 – 15
Typisierung
Datentyp als Wertemenge mit Operationen darauf
Typbindung von Variablen und Ausdrücken
Sprachdefinierte einfache Datentypen für Zahlen und Zeichen
Anweisungsstrukturierung
Strukturierte Anweisungen für
Sequenzen,
Alternativen (if-then-else, case) und
Iterationen (while, until, for)
Datenstrukturierung
Strukturierte Datentypen, die sich rekursiv aus anderen Datentypen zusammensetzen, wie
- Reihungen und
- Verbunde
Programmiererdefinierte Datentypen durch Typkonstruktoren
Algorithmenabstraktion
Prozeduren
Parameter
Rekursive Prozeduraufrufe
Datenabstraktion
ADSen und ADTen
In benannten Einheiten gekapselte Daten und Algorithmen
Schnittstelle und Implementation getrennt
Konkrete Daten hinter der Schnittstelle verborgen
Schnittstelle auf Operationen beschränkt
Abstrakte Daten durch Effekt von Zugriffsoperationen definiert
Verhaltensabstraktion
Vererbung oder Erweiterung zwischen ADTen macht diese zu Klassen
Klassifizieren durch Generalisieren und Spezialisieren
Abstrakte Klassen: nur partiell implementiert
Polymorphie: Objekte austauschbar
Generizität
Abstrahieren von verwendeten Datentypen
Parametrisieren von Programmeinheiten mit Typen
Bibliotheken (library)
Fertige benutzbare Lösungen für bekannte Teilprobleme
Erweitern die Ausdrucksstärke von Sprachen
Entwurfsmuster (design pattern)
Anpassbare Lösungsstrukturen für ähnlich wiederkehrende Teilproblemarten
Erweitern die Fähigkeiten von Programmierern
Rohbauten (framework)
Unvollständige erweiterbare Lösungen von Problemarten, zu denen Teilprobleme durch Erweiterungen zu lösen sind
Erweitern die Fähigkeiten von Entwicklerteams und Firmen
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 16
1 Einführung
1.4
Soll und Ist von Programmiersprachen
Prinzipiell sind bei Programmiersprachen zwei Aspekte zu unterscheiden:
Die Beschreibung einer Programmiersprache umfasst die syntaktischen, semantischen und pragmatischen Eigenschaften damit formulierter Programme. Sprachbeschreibungen bestehen aus Dokumenten, die Texte und Grafiken enthalten.
Die Implementation einer Programmiersprache umfasst die Umformung damit formulierter Quell- in Zielprogramme und ggf. deren Ausführung. Sprachimplementationen bestehen aus Werkzeugen zum Erstellen, Umformen, Ausführen und Testen
von Programmen.
Bild 1.11 gibt einen Überblick zu verschiedenen Teilaspekten. Weitere Aspekte sind
Vielfalt und Einheit sowie Umgebungen von Programmiersprachen.
Bild 1.11 Aspekte von Programmiersprachen
Programmiersprache
Beschreibung
Pragmatik
menschliche
Pragmatik
Implementation
Semantik
mechanische
Pragmatik
dynamische
Semantik
Syntax
statische
Semantik
Syntax
Lexik
Darstellung
in der Zielsprache
Zahlen
Zeichen
Befehle
Werkzeuge
Editor
Browser
GUI-Builder
Übersetzer
Kompilierer
1.4.1
Binder
Lader
Laufzeitsystem
Debugger
Interpretierer
Beschreibung von Programmiersprachen
Die Beschreibung von Programmiersprachen teilt sich in die Aspekte Syntax, Semantik
und Pragmatik.
1.4.1.1
Pragmatik
Die Pragmatik einer Programmiersprache behandelt das Verhältnis der Zeichen einerseits zu Menschen, andererseits zu Rechnern: Welche Ideen drücken die Zeichen aus,
wie verstehen und benutzen Menschen diese Zeichen? Was bewirken sie im Rechner?
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.4 Soll und Ist von Programmiersprachen
1 – 17
Bild 1.12
Pragmatik
menschliche
Pragmatik
Programmiersprache
mechanische
Pragmatik
Die menschliche Pragmatik untersucht Fragen wie Lesbarkeit, Verständlichkeit,
Lehrbarkeit und Lernbarkeit von Programmiersprachen sowie ihre Anwendbarkeit
und ihren Nutzen zur Lösung praktischer Aufgaben.
Die mechanische Pragmatik untersucht Fragen wie die Übersetzbarkeit von Programmiersprachen, ihre Anforderungen an Betriebssysteme und Abhängigkeiten
von Rechnerarchitekturen.
Pragmatische Aspekte sind kaum formalisierbar, daher werden sie verbal beschrieben.
Die Pragmatik ist Gegenstand der Diskussion, Entwicklung und Forschung. Sie ist eng
verknüpft mit den Qualitätsmerkmalen, auf die 1.7 eingeht.
1.4.1.2
Semantik
Die Semantik einer Programmiersprache behandelt die Bedeutung der Zeichen und
ihre Beziehungen zu den Objekten, auf die sie anwendbar sind. Sie legt fest, was welches Sprachelement oder -konstrukt bedeutet und welche Wirkung es in einem Programmablauf hervorruft. Die Semantik wird beschrieben durch eine Menge von Verhaltensregeln, die die Funktionsweise von Programmen bestimmen. Man unterscheidet
dynamische und statische Aspekte.
Die dynamische Semantik befasst sich mit Bedeutungen von Sprachelementen zur
Laufzeit von Programmen. Dazu gehören z.B. Antworten auf die Fragen:
Wie werden Daten Speicherplätze zugeordnet?
Wie werden Ausdrücke ausgewertet?
Wie werden Anweisungen ausgeführt?
Die statische Semantik befasst sich mit Bedeutungen von Sprachelementen zur
Übersetzungszeit von Programmen – also ohne dass ein Programm laufen muss.
Dazu gehören z.B.
Kontextregeln (die die Syntax nicht ausdrückt),
Sichtbarkeits- und Zugriffsregeln (Schachtelung, Rechte),
Typregeln (Verträglichkeit, Anpassung).
Bei manchen (meist älteren) Programmiersprachen ist die Semantik nicht formalisiert,
sondern nur verbal beschrieben. Andere (meist jüngere) Programmiersprachen haben
eine weitgehend formalisierte Semantik, denn dafür wurden theoretisch fundierte
Methoden entwickelt. Drei wichtige Ansätze zur Formalisierung sind
die operationale Semantik,
die axiomatische Semantik, und
die denotationale Semantik.
1.4.1.3
Syntax
Die Syntax (im weiten Sinn) einer Programmiersprache behandelt Beziehungen der
Zeichen untereinander, ihre Kombinierbarkeit ohne Rücksicht auf ihre spezielle Bedeutung und ihre Beziehung zur Umgebung. Sie legt fest, welche Sprachelemente und konstrukte es gibt und wie diese sich zusammensetzen. Die Syntax wird beschrieben
durch Regeln, die die Struktur von Programmen bestimmen. Man unterscheidet zwei
Ebenen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 18
1 Einführung
Die Syntax (im engen Sinn)
definiert die als Nichtterminale oder syntaktische Einheiten bezeichneten
zusammengesetzten Arten bedeutungstragender Komponenten der Sprache,
beschreibt ihre Strukturen durch Grammatiken oder Syntaxdiagramme, und
legt allgemeine syntaktische Regeln fest.
Die Lexik
definiert die als Symbole oder lexikalische Einheiten bezeichneten elementaren
Arten bedeutungstragender Komponenten der Sprache,
beschreibt ihre Wertemengen durch reguläre Ausdrücke, und
legt allgemeine lexikalische Regeln fest.
Die Symbole einer Sprache sind ihre terminalen syntaktischen Einheiten und heißen
daher auch kurz Terminale. Die Werte der Symbole heißen auch Token.
Die Syntax der meisten Programmiersprachen ist weitgehend bis vollständig formalisiert. Notationen für syntaktische Regeln sind
reguläre Ausdrücke,
Syntaxdiagramme, und
Grammatiken, vor allem in speziellen Ausprägungen als
erweiterte Backus-Naur-Formen (EBNF).1, 2
Diese Notationen seien hier als bekannt vorausgesetzt. Die Grenze zwischen Syntax
und Semantik ist unscharf. Es ist möglich, gewisse Eigenschaften einer Sprache alternativ als semantisch oder syntaktisch festzulegen.
1.4.2
Implementation von Programmiersprachen
Die Implementation von Programmiersprachen teilt sich in die Aspekte Darstellung in
der Zielsprache, Werkzeuge, und Implementationskonzepte.
1.4.2.1
Darstellung in der Zielsprache
Die Darstellung der Quell- in der Zielsprache, insbesondere in der Maschinensprache (oder im Rechner) beantwortet die Fragen:
Wie ist die Zielsprache definiert?
Wie werden Literale der Quellsprache in der Zielsprache dargestellt? Gibt es dazu
Standards, Normen?
In welchen Formaten werden Zahlen gespeichert und verarbeitet?
Mit welchen Zeichencodes werden Zeichen codiert?
Wie werden andere Elemente der Quellsprache auf Elemente der Zielsprache, insbesondere auf Befehle der Maschinensprache, abgebildet?
Dieser Aspekt lässt sich nicht eindeutig der Beschreibung oder der Implementation von
Programmiersprachen zuordnen. So legt etwa die eine Sprache die Darstellung von
Zahlen und Zeichen in ihrer Sprachbeschreibung fest, die andere überlässt dies ihren
Sprachimplementationen.
1
John Warner Backus (geb. 1924), US-amerikanischer Informatik-Pionier, entwickelte für
IBM die Programmiersprachen Speedcoding (1949) und Fortran (1954), schuf mit P. Naur die
Backus-Naur-Form, arbeitete an den function-level-Programmiersprachen FP und FL.
2
Peter Naur (geb. 1928), dänischer Informatik-Pionier, schuf mit J. Backus die Backus-NaurForm, wirkte an der Entwicklung der Programmiersprache Algol 60 mit, kritisierte in seinem
Buch Computing: A Human Activity (1992) die formalistische Schule des Programmierens.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.4 Soll und Ist von Programmiersprachen
1.4.2.2
1 – 19
Werkzeuge
Die Werkzeuge (tool) zu einer Sprachimplementation umfassen die in Bild 1.11
genannten:
Entwicklungsumgebung
Texteditor zum Editieren des Quellprogrammtexts,
Browser zum Stöbern in Bibliotheken und Erkunden von Schnittstellen von Modulen, Klassen und Komponenten,
GUI-Builder zum Editieren des Layouts grafischer Benutzungsoberflächen und
Generieren von Quelltextschablonen,
Übersetzer (translator) zum Umformen des Quelltexts in die Zielsprache,
Binder (linker) zum Binden der Objektcodeeinheiten getrennt übersetzter Quelltexteinheiten,
Lader (loader) zum Laden des Maschinencodes in den Hauptspeicher,
Laufzeitsystem (runtime system) als unmittelbare Umgebung des Maschinencodes,
Debugger als Testhilfe für z.B. Einzelschrittablaufverfolgung (single stepping, tracing), Haltepunkte (breakpoint), Diagnoseausgabe bei erkannten Fehlern.
Früher koexistierten diese Werkzeuge relativ unabhängig voneinander als kommandozeilenorientierte Programme. In den 1980er Jahren erschienen erste integrierte Entwicklungsumgebungen (integrated development environment, IDE) wie Turbo Pascal. IDEs sind Anwendungsprogramme, die wichtige Werkzeuge vereinen, um
Softwareentwicklern die Arbeit zu erleichtern und sie von Routineaufgaben zu entlasten. Heute existieren zu den meisten Programmiersprachen mehr oder weniger mächtige visuelle IDEs, d.h. mit grafischen Benutzungsoberflächen. Moderne IDEs unterstützen auch Aspekte wie
Quellprogrammverarbeitung: Generieren von Quelltextschablonen für Sprachkonstrukte, farbiges Hervorheben der Syntax, syntaxgesteuertes Editieren, halbautomatisches Ergänzen von Fragmenten, Formatieren und Analysieren von Quelltexten,
Extrahieren von Schnittstellen, Spezifikationen, Dokumentationsteilen, Generieren
von Diagrammen wie Struktogrammen und UML-Modellen aus Quelltexten;
UML-Modellierung: grafische Editoren zum Erstellen von UML-Modellen und
Generatoren, die aus UML-Modellen Quelltextschablonen erzeugen;
Entwurfsmuster: Generieren von Quelltextschablonen für benötigte Klassen;
Datenbankanbindung;
Komponentenerzeugung;
Versionsverwaltung;
Projektmanagement.
Eine IDE kann auch mehrere Programmiersprachen unterstützen, z.B. Eclipse.
1.4.2.3
Implementationskonzepte
Übersetzen
Interpretieren
Kompilieren
Binden
Vorverarbeiten
Kompilationsphasen
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 20
1.4.3
1 Einführung
Vielfalt und Einheit von Programmiersprachen
Entstehungsdaten
Varianten
Teilmengen: Auswahl von Elementen und Merkmalen der Originalsprache
Erweiterungen: zusätzliche Elemente und Merkmale
Dialekte: geänderte Elemente und Merkmale
Familien
Versionen: folgen zeitlich aufeinander
Standards (herstellereigene Standards, Konsensstandards)
Referenzmanual
Sprachhandbuch
1.4.4
Umgebungen von Programmiersprachen
Bibliotheken
Rohbauten
1.5
Einflüsse auf und von Programmiersprachen
Keine Programmiersprache entsteht aus nichts, keine existiert ewig. Wie alles, was
Menschen schaffen, hat jede Programmiersprache ihre Vorgeschichte und Einflüsse,
ihre Nutzungsdauer und Weiterentwicklung, und ihre Wirkungen. „Die richtigen Programmierkonzepte“ und „die richtige Programmiersprache“ kann es nicht geben, darauf hat schon C. A. R. Hoare hingewiesen.
1.5.1
Einflüsse auf Programmiersprachen
Jeder Entwerfer einer Programmiersprache muss viele Aspekte berücksichtigen
[PrZ98] S. 31:
den intendierten Anwendungsbereich mit seinen Anforderungen,
die zugrunde gelegten Plattformen – Rechnerarchitekturen und Betriebssysteme –
mit ihrer Funktionalität (z.B. E/A-Modell: Stapel- oder Dialogverarbeitung oder
GUI),
die zu benutzenden Standards und Normen,
das gewählte Programmierparadigma,
den Entwicklungsstand der
theoretischen Grundlagen der Informatik,
Softwareentwicklungsmethodik (z.B. Spezifikationsmethode),
Programmiersprachen mit den Methoden ihrer Beschreibung und Implementation (z.B. Kompiliertechniken),
die vorhandenen Ressourcen (Arbeitskräfte, Zeit, Geld).
1.5.2
Einflüsse von Programmiersprachen
Innovation
Verbreitung
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.6 Entwicklung imperativer Sprachen
1.6
1 – 21
Entwicklung imperativer Sprachen
Aus der Entwicklungsgeschichte der Programmiersprachen zeigt Bild 1.13 einen kleinen, hier relevanten Ausschnitt, der sich auf einige einflussreiche oder verbreitete
imperative Sprachen beschränkt. Ein Pfeil mit durchgezogener Linie bedeutet eine Entwicklungslinie oder einen starken Einfluss, ein Pfeil mit einer gestrichelten Linie einen
schwächeren Einfluss.
Component Pascal
Niklaus Wirth und Jürg Gutknecht starteten 1985 ein Projekt namens Oberon, aus dem
das in der Oberon-Sprache geschriebene Oberon-System hervorging. Die Sprache
Oberon ist eine Weiterentwicklung der von Wirth entworfenen Programmiersprachen
Pascal und Modula-2; sie übernimmt von Pascal Grundkonzepte der strukturierten
Programmierung, von Modula-2 Grundkonzepte der modularen Programmierung und
bietet gegenüber ihren Vorgängersprachen zusätzlich objektorientierte Konzepte.
Oberon-2 ist eine aufwärtskompatible Erweiterung von Oberon, von Wirth und Hanspeter Mössenböck 1991 entwickelt. Die Firma Oberon microsystems Inc. erweiterte
Oberon-2 um komponentenorientierte Konzepte zu Component Pascal.
Eiffel
Eiffel wurde 1985 von Bertrand Meyer bei Interactive Software Engineering Inc. als
neue objektorientierte Sprache entworfen, die erste Implementation lag 1986 vor. 1988
erschien Meyers Buch Object-Oriented Software Construction, in dem er den Sprachentwurf vorstellte [Mey90]. Nach einigen Änderungen wurden Eiffel und umfangreiche
Klassenbibliotheken 1995 standardisiert. Es gibt mehrere Implementationen und Entwicklungsumgebungen auf den wichtigsten Plattformen.
C
C, dessen Spuren C++, Java, C#, D, Scala, Go sowie viele Skriptsprachen tragen,
wurde um 1972 von Dennis Ritchie für eine Reimplementation des Betriebssystems
Unix entworfen. Es war ursprünglich typlos, sollte eine in hardwarenaher Programmierung verwendete Assemblersprache ablösen, und Portierbarkeit von Systemprogrammen und Erzeugung effizienten Maschinencodes gewährleisten.
C++
C++ wurde von Bjarne Stroustrup in den AT&T Bell Laboratories von 1980 bis 1983
als Erweiterung von C entwickelt, von 1983 bis 1986 veröffentlicht und in den Folgejahren mehrfach geändert und erweitert. Es gibt zahlreiche herstellerspezifische Dialekte von C++. Die Hauptentwicklungslinie stabilisierte sich Mitte der 1990er Jahre in
einem ANSI- und ISO-Standard. Es gibt über 12 unabhängige Übersetzer, C++ steht
auf praktisch allen Plattformen mit vielen Werkzeugen bereit.
Java
Java hieß ursprünglich Oak und war für den Einsatz in eingebetteten Systemen vorgesehen, bevor es auf Internet-Anwendungen orientiert wurde. Entwerfer von Java sind
James Gosling, Bill Joy, Guy Steele und andere. Java wurde 1995 von Sun Microsystems mit gewaltigem, bisher bei Programmiersprachen unbekanntem Marketingaufwand als die Sprache für Web-Anwendungen, als besseres C++ und als Kampfansage
an Microsoft veröffentlicht.
C#
C# ist Microsofts Antwort auf Java und jahrelange Rechtsstreitereien mit Sun. Die Entwerfer von C# sind Anders Hejlsberg, Scott Wiltamuth und Peter Golde. Hejlsberg war
zuvor bei Borland Chef-Designer für Turbo Pascal und Delphi, deren Einfluss auf C#
sichtbar ist. Die Sprachspezifikation und die erste Implementation wurden im Juli 2000
zusammen mit der Ankündigung der .NET-Plattform veröffentlicht. Die ECMA verabschiedete im Dezember 2002 einen C#-Standard [ECMA334].
D
D wird seit 1999 von Walter Bright bei Digital Mars als besseres C++ für Systemprogrammierung entwickelt [D09]. D übernimmt von Oberon Module und von Eiffel Vertragskonstrukte.
Scala
Scala für „scalable language“ wird seit 2001 von Martin Odersky an der École Polytechnique Fédérale de Lausanne als Kombination objektorientierter und funktionaler
Elemente entwickelt und seit 2011 vermarktet [Sca10].
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 22
1 Einführung
Bild 1.13 Entwicklungslinien imperativer Programmiersprachen
1950
Assemblersprachen
Fortran
1955
1960
Algol 60
Cobol
PL/I
1965
BCPL
Simula 67
Algol 68
1970
Pascal
C
1975
1980
Smalltalk 80
Modula-2
Ada
Turbo
Pascal
C++
1985
Eiffel
1990
Object
Pascal
ANSI-C
Oberon-2
Ada 95
Java
1995
Component
Pascal
C++ ISO
2000
D
C#
Delphi
Scala
Spec#
2005
2010
Go
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.7 Merkmale von Programmiersprachen
1 – 23
Spec#
Spec# ist eine Erweiterung von C# zu einer Vertragsspezifikationssprache mit Eiffels
Invarianten, Vor- und Nachbedingungen, die von Microsoft Research entwickelt wird
und seit 2003 in die Entwicklungsumgebung Microsoft Visual Studio für die .NET-Plattform integriert ist [Spe12].
Go
Go wurde von Robert Griesemer, Rob Pike und Ken Thompson bei Google Inc. als
Sprache zur Programmierung verteilter Systeme entwickelt und 2009 veröffentlicht
[Go10]. Das klassenlose Go kombiniert Javas Interfaces mit Oberons Verbunden und
typgebundenen Prozeduren und übernimmt Konzepte nebenläufiger Programmierung
von Hoares Communicating Sequential Processes (1978), bietet aber noch nicht einmal
Zusicherungen.
1.7
Merkmale von Programmiersprachen
1.7.1
Nichttechnische Merkmale
zu ergänzen
1.7.2
Technische Merkmale
funktional: Paradigma, Abstraktionsebene, weitere Programmierkonzepte und Sprachkonstrukte
Qualitätsmerkmale stellen sich selten zufällig ein. Deshalb formulieren Sprachentwerfer oft bestimmte Qualitätsmerkmale als Entwurfsziele. Siehe dazu [Lou93] S. 47ff.
und [Woo96] S. 21ff.
1.7.3
Qualitätsmerkmale von Sprachbeschreibungen
Problemlösungsmächtigkeit
Ausdrucksstärke (expressiveness)
Prägnanz (conciseness)
Schreibbarkeit (writability)
Lesbarkeit (readability)
Verständlichkeit
Benutzbarkeit
Lernbarkeit
Kompatibilität mit etablierten Konventionen und Notationen
Zuverlässigkeit
Sicherheit (safety)
Speichersicherheit
Typsicherheit
Modulsicherheit
Robustheit (robustness)
Erkennen von Programmierfehlern
Vermeiden von Unstetigkeiten
Abschrecken vor Missinterpretationen und Missbrauch
Wartbarkeit der Programme (maintainability)
Umfang (size)
Bestimmtheit (definiteness) und Genauigkeit (preciseness) der Sprachdefinition
Wohldefiniertheit der syntaktischen und semantischen Beschreibungen
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 24
Fortran-Schleife
1.7.4
1 Einführung
Ein Beispiel verdeutlicht, wie wichtig dieses Merkmal ist. Die erste Version von
Fortran führte ein Konstrukt für eine Bedingungsschleife in die Programmierung ein, legte in der Sprachbeschreibung aber nicht fest, wann die Bedingung
ausgewertet werden soll. In heutiger Terminologie: Es blieb offen, ob es sich um
eine kopf- oder fußgesteuerte Bedingungsschleife handelt. Als Folge davon entschieden sich Sprachimplementierer für die eine oder andere Variante. Portierte
Fortran-Programme produzierten Überraschungen. Dasselbe Programm lieferte
auf verschiedenen Rechnern verschiedene Ergebnisse. Heute ist klar, dass unterschiedliche Schleifensemantiken den Effekt von Programmen beeinflussen.
Vollständige formale Definition
Beweisbarkeit der Korrektheit von Programmen
Standardisierung
Konzeptuelle Einfachheit (simplicity)
Vermeiden unnötiger Komplexität
Vermeiden unnötiger Redundanz, „eine gute Lösung für jedes Problem“
Einheitlichkeit, Regularität
Vermeiden von Ausnahmen zu Regeln
Vermeiden quantitativer Beschränkungen
Konsistenz
Ähnliche Konzepte ähnlich realisieren
Unterschiedliche Konzepte unterschiedlich realisieren
Allgemeinheit
Orthogonalität (orthogonality): Grundkonzepte voneinander unabhängig und beliebig kombinierbar
Abstraktionsgrad
Portierbarkeit
Maschinenunabhängigkeit
Verknüpfbarkeit (interoperability)
Schneller Zugriff auf Bibliotheken in anderen Sprachen
Modularität
Einschränkbarkeit auf Teilsprachen
Erweiterbarkeit um weitere Sprachkonzepte
Effiziente Übersetzbarkeit von Quellprogrammen
Effiziente Implementierbarkeit der Sprachkonstrukte
Qualitätsmerkmale von Sprachimplementationen
Effiziente Übersetzung von Quellprogrammen
Schnelle Übersetzung von Quellprogrammen
Qualität des erzeugten Objektcodes
Zuverlässige Implementation der Sprachkonstrukte
Effiziente Implementation der Sprachkonstrukte
Niedrige Entwicklungskosten
Systematische, schnelle Implementierbarkeit
Portierbarkeit
Adaptierbarkeit
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.8 Überblick über die Auswahlsprachen
1.8
1 – 25
Überblick über die Auswahlsprachen
Zunächst seien die Auswahlsprachen Component Pascal, Eiffel, C++, Java, C#.
anhand der Klassifikationskriterien von 1.1 geordnet.
Tabelle 1.2 Klassifikation der Auswahlsprachen
Klassifikationskriterium
Component
Pascal
Abstraktionsebene
Eiffel
C++
Java
C#
höhere Programmiersprache
Mehrzweck
Anwendungsbereich
Lehre, Systemprogrammierung
Rolle im Softwareentwicklungsprozess
Implementation
Systemprogrammierung
Spezifikation +
Implementation
Programmierparadigma
Implementation
imperativ
Notation
textuell
Ausführbarkeit
maschinell ausführbar
Ausführendes Organ
Implementationskonzept
1.8.1
Web
Prozessor
virtuelle Maschine
Kompilation
Kompilation +
Interpretation
Ziele der Sprachentwürfe
Sprachen werden zu bestimmten Zwecken entworfen. Welche Ziele standen den Entwerfern der Auswahlsprachen vor Augen?
Component Pascal
Oberon entstand wegen Schwächen von Modula-2, es übernimmt dessen Stärken wie
strenge Typbindung und Unterstützung für strukturiertes, prozedurales und modulares
Programmieren. Neues Hauptziel ist die Unterstützung erweiterbaren Programmierens,
erreicht durch Typerweiterung (Vererbung). Außerdem soll Oberon wirklich maschinenunabhängig, einfacher und einheitlicher als Modula-2 sein. Bei Oberon wurde das
Prinzip des ökonomischen Designs, d.h. die Beschränkung auf Wesentliches angewandt. Die Erweiterungen von Component Pascal beziehen sich auf das Programmieren von Systemen mit dynamisch ladbaren Komponenten.
Eiffel
Der Entwurf von Eiffel ist softwaretechnisch begründet: Eiffel soll sich für komplexe
Softwaresysteme mit hohen Qualitätsanforderungen eignen. Als einfache, rein objektorientierte Sprache mit strengem Typkonzept, Zusicherungen, disziplinierter Ausnahmebehandlung, Generizität usw. soll es die Entwicklung von zuverlässiger, korrekter,
robuster, fehlertoleranter, erweiterbarer, wiederverwendbarer, portierbarer, effizienter
Software ermöglichen. Außerdem soll es die Reversierbarkeit des Entwicklungsprozesses unterstützen.
C++
Stroustrup beschreibt folgende Entwurfsziele für C++ [Str97], [Str94a].
„A general purpose language designed to make programming more enjoyable for
the serious programmer.“
C++ soll „a better C“ sein.
Kompatibilität mit C: Alle C-Programme sollen als C++ kompilierbar und mit CBibliotheken bindbar sein. Es wird nicht versucht, Probleme durch Entfernen von
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 26
Java
1 Einführung
Spracheigenschaften zu lösen, z.B. durch Säubern der Syntax. Spätere Einschränkung: So nahe an C wie möglich, aber nicht näher.
Gründe dafür: C ist weit verbreitet, flexibel, effizient, portierbar. Gegenüber diesen
Vorteilen sind Nachteile von C wie die Sicherheitslücken und die seltsame Syntax
von Vereinbarungen nachrangig.
„Nahe an der Maschine ...“:
hardwarenahe Mechanismen, Effizienz, Vermeidung von Laufzeitaufwand;
Einsetzbarkeit in traditionellen Kompilierer-, Binder- und Laufzeitsystemen;
Portierbarkeit.
„... und nahe am zu lösenden Problem“:
Abstraktionsmechanismen, Datenabstraktion und objektorientierte Programmierung;
Typsystem mit statischer Typprüfung;
Werkzeug für Bibliotheken programmiererdefinierter Typen hoher Qualität, die
handlich, sicher und effizient zu benutzen sind;
Vorrang für programmiererdefinierte Typen, die aber wie sprachdefinierte Typen
benutzbar sind.
C++ ist nur eine Sprache innerhalb eines Systems, mit Schnittstellen zu anderen
Sprachen und Systemkomponenten.
Einfachheit der Sprache wird auch als Entwurfsziel genannt.
Leicht genug lernbar, um Benutzer anzusprechen.
Leicht genug implementierbar, um Implementierer anzusprechen, d.h. mit Algorithmen nicht komplizierter als lineares Suchen.
Sun beschreibt Java in einem frühen Papier so [Sal98] S. 778:
„A simple, object-oriented, distributed, interpreted, robust, secure, architecture neutral, portable, high-performance, multithreaded, and dynamic language.“
Gosling, Joy und Steele schreiben [GJS96]:
C#
„Java is a general-purpose, concurrent, class-based, object-oriented programming
language, specifically designed to have as few implementation dependencies as possible. Java allows application developers to write a program once and then be able
to run it everywhere on the Internet.“
Wiltamuth und Hejlsberg schreiben [WiH04]:
„C# (pronounced "C sharp") is a simple, modern, object-oriented, and type-safe programming language. It will immediately be familiar to C and C++ programmers.
C# combines the high productivity of Rapid Application Development (RAD) languages and the raw power of C++.“
Der Vorspann des ECMA-Standards nennt zudem [ECMA334]:
Unterstützung für softwaretechnische Prinzipien wie strenge Typprüfungen, Prüfungen von Reihungsindexgrenzen, Entdecken von Zugriffen auf uninitialisierte Variablen, automatische Speicherbereinigung, Robustheit, Haltbarkeit, Produktivität.
Entwicklung von Softwarekomponenten für verteilte Umgebungen.
Portierbarkeit von Quellcode und Programmierern, besonders von C/C++-Programmierern.
Unterstützung für Internationalisierung.
Eignung für ein breites Anwendungsspektrum auf Plattformen vom eingebetteten
System zum Großrechner, vom kleinen zum großen Betriebssystem.
Effizienz; C# soll dabei aber nicht mit C oder Assembler konkurrieren.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.8 Überblick über die Auswahlsprachen
1.8.2
1 – 27
Quantitative Eigenschaften
Lassen sich Programmiersprachen messen? Kaum – nur einzelne Merkmale lassen sich
in Zahlen ausdrücken.
Tabelle 1.3 Quantitative Eigenschaften
Eigenschaft
Component
Pascal
Eiffel
C++
Fassung:
Urfassung: 156
Langf.: 594
annotiert: 570
Kurzf.: 100
Standard: > 700
Java
C#
> 505
471
Umfang des
Sprachreferenzmanuals in Seiten
29 – 34
Syntax: Anzahl der
Nichtterminale
(ohne lexikalische
Elemente)
34
154
125
81
271
spezielle Wörter,
daruntera
76
63
74
52
78
reservierte Wörter,
darunter
40
63
73
48
77
8
2
2
9
2
1
9
1
1
15
3
1
1
4
1
1
3
1
1
sprachdefinierte
Typen
Werte
Größen
Schlüsselwörter,
darunter
sprachdefinierte
Typen
Werte
Größen
Prozeduren
36
12
3
21
spezielle Symbole
27
27
46
46
45
Operatorsymbole
19
25
54
37
44
OperatorPrioritätsstufen
4
12
18
14
14
a
Die Begriffe „reserviertes Wort“ und „Schlüsselwort“ werden in der Literatur nicht einheitlich gebraucht. Hier gilt: Ein
reserviertes Wort kann nicht als Name benutzt werden. Ein Schlüsselwort ist nur in speziellen Kontexten ein spezielles
Wort.
Aufgabe 1.1
Zahlen interpretieren
27.9.12
Welche Eigenschaften der Sprachen sind Tabelle 1.3 zu entnehmen?
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 28
1.8.3
1 Einführung
Grundstrukturen von Programmen
Hier seien die syntaktischen Strukturen der Auswahlsprachen betrachtet.
Component Pascal
Component Pascal ist eine modulare Sprache: Ein Component-Pascal-Quellprogramm
besteht aus einer Menge von Modulen.
MODULE Modulname;
Importabschnitt
Vereinbarungen von Konstanten, Typen, Variablen, Prozeduren
BEGIN
Initialisierungen
CLOSE
Finalisierungen
END Modulname.
Mindestens eines der Module eines Programms muss mindestens eine Prozedur exportieren, bei der die Ausführung des Programms beginnen kann. Ein Programm kann
beliebig viele Einstiegsprozeduren haben. Ein Aspekt der Komponentenorientierung
von Component Pascal ist, dass die getrennt kompilierten Module bei Bedarf dynamisch gebunden und geladen werden.
Eiffel
Eiffel ist eine rein objektorientierte Sprache: Ein Eiffel-Quellprogramm besteht aus
einer Menge von Klassen.
class Klassenname
deklarative Abschnitte
feature
Vereinbarungen von Merkmalen (Abfragen, Kommandos)
invariant
Klasseninvarianten
end
Zur Ausführung eines Eiffel-Programms, in Eiffel-Terminologie eines Systems, gehört
die Angabe einer Wurzelklasse und einer Erzeugungsprozedur, zur Laufzeit existiert
eine dynamische Menge von Objekten. Ein Programm kann beliebig viele Einstiegsprozeduren haben. Klassen werden getrennt kompiliert und statisch gebunden.
C++
C++ ist als Erweiterung von C zunächst eine prozedurale (funktionsorientierte, aber
nicht funktionale!) Sprache: Ein C++-Programm besteht als Quelltext aus Vereinbarungen von Daten und Funktionen. Zusätzlich enthält es Direktiven für den von C übernommenen Vorübersetzer (preprocessor), auf die nicht ganz verzichtet werden kann.
Vorübersetzerdirektiven
Vereinbarungen von Konstanten, Typen, Variablen, Funktionen
int main () {
Vereinbarungen
Anweisungen
return ganzzahliger Ausdruck;
}
Es gibt nur eine einzige Einstiegsfunktion. Sie heißt stets main, kann Parameter besitzen und liefert stets einen ganzzahligen Ergebniswert an die aufrufende Umgebung.
Programmteile werden unabhängig voneinander kompiliert und statisch gebunden.
Eine von C geprägte syntaktische Eigenheit ist das Blocken mit geschweiften Klammern. Da C++, Java, C# und andere Sprachen das übernommen haben, heißen sie auch
{}-Sprachen.
Java
Java ist eine rein objektorientierte Sprache: Ein Quellprogramm besteht aus einer
Menge von Klassen und Schnittstellen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.8 Überblick über die Auswahlsprachen
1 – 29
class Klassenname {
Vereinbarungen von
Feldern,
Methoden,
geschachtelten Klassen und Schnittstellen,
Initialisierern,
Konstruktoren
}
interface Schnittstellenname {
Vereinbarungen von
Konstanten,
abstrakten Methoden,
geschachtelten Klassen und Schnittstellen
}
Zur Ausführung eines Programms gehört die Angabe einer Wurzelklasse, die eine
Methode namens main enthalten muss, bei der der Ablauf startet. Klassen und Schnittstellen werden getrennt kompiliert. Klassen werden dynamisch gebunden und geladen.
C#
C# ist eine rein objektorientierte Sprache, die ihre Abstammung von Java nicht verleugnen kann: Ein Quellprogramm besteht aus einer Menge von Klassen und Schnittstellen.
class Klassenname {
Vereinbarungen von
Daten (Konstanten, Felder),
Funktionen (Methoden, Eigenschaften, Ereignisse, Indexer,
Operatoren, Konstruktoren, Destruktoren),
geschachtelten Typen
}
interface Schnittstellenname {
Vereinbarungen von
Methoden, Eigenschaften, Ereignissen, Indexern
}
In C# gehört wie in Java zur Ausführung eines Programms die Angabe einer Wurzelklasse, die eine Methode namens Main enthalten muss, bei der der Ablauf startet. Klassen und Schnittstellen werden getrennt kompiliert. Klassen können dynamisch gebunden und geladen werden.
Als erstes Beispiel zeigt Tabelle 1.4 primitivst mögliche Programme.
Tabelle 1.4 Minimale Programme
Component Pascal
MODULE Nothing;
END Nothing.
Eiffel
class NOTHING
end
C++
int main () {}
Java
C#
class Nothing {
}
class Nothing {
}
1.8.4
Unterstützte Programmierstile, Abstraktionsebenen, Sprachkonzepte
Programmierstil
Ein Aspekt ist, wie weit eine Sprache bestimmte Programmierstile ermöglicht oder
unterstützt. Eine Sprache ermöglicht einen Programmierstil, wenn sie Sprachkonstrukte bereitstellt, die unter geeigneten Richtlinien eingesetzt das Programmieren in diesem Stil erlauben. Eine Sprache unterstützt einen Programmierstil, wenn sie Sprachkonstrukte für diesen Stil bereitstellt, zum Programmieren in diesem Stil ermutigt und
es gleichzeitig erschwert oder verhindert, auf eine Weise zu programmieren, die diesem
Stil widerspricht. Grenzen sind freilich fließend. Tabelle 1.5 bezieht sich auf die Programmierstile von 1.2.1 und verwendet diese Symbole:
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 30
1 Einführung
Programmierstil nicht unterstützt, nicht möglich.
Programmierstil nicht unterstützt, aber eingeschränkt möglich mit Nachteilen.
☺ Programmierstil unterstützt.
Tabelle 1.5 Unterstützte Programmierstile
Component
Pascal
Eiffel
Strukturiert
☺
☺
Prozedural
☺
Modular
☺
ADS
☺
ADT
☺
☺
Objektorientiert
☺
☺
Komponentenorientiert
☺
Programmierstil
Generisch
Nebenläufig
Bibliotheken
C++
Java
C#
☺
☺
☺
☺
☺
☺
☺
☺
☺
☺
☺
☺
☺
☺
Bibliotheken
☺
☺
Betrachtet man die Programmierstile prozedural, modular und objektorientiert als
Alternativen, so gilt:
Component Pascal und C++ sind hybride Multistilsprachen:
Component Pascal unterstützt prozedurales, modulares und objektorientiertes
Programmieren.
C++ unterstützt prozedurales und objektorientiertes Programmieren und ermöglicht modulares Programmieren.
Eiffel, Java und C# sind eher Monostilsprachen, sie unterstützen objektorientiertes
Programmieren.
Component Pascal deckt alle Stile bis auf generisches und nebenläufiges Programmieren ab. Generisches Programmieren mit Eiffel und C* zeigen die Kapitel 3 und 4. Zum
Kennenlernen nebenläufigen Programmierens konzentriert sich der zweite Teil der Vorlesung Informatik 3 auf Java.
Abstraktionsebene
Sprachmerkmal
Ein zweiter Aspekt ist, welche Abstraktionsebenen eine Sprache erreicht. Tabelle 1.6
bezieht sich auf Abstraktionsebenen von 1.3 und verwendet diese Symbole:
Abstraktionsebene verdeckt.
Abstraktionsebene nachteilig erreicht oder schwach unterstützt.
☺ Abstraktionsebene gut erreicht.
Zum dritten Aspekt, in welchen besonderen Merkmalen sich die Sprachen unterscheiden, verwendet Tabelle 1.7 diese Symbole:
Nützliches Sprachmerkmal leider nicht vorhanden, ggf. eingeschränkt simulierbar.
Nützliches Sprachmerkmal nicht vorhanden, aber näherungsweise simulierbar.
☺ Nützliches Sprachmerkmal vorhanden.
Ohne Smiley: Sprachmerkmal beschränkt nützlich oder fragwürdig.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.8 Überblick über die Auswahlsprachen
1 – 31
Tabelle 1.6 Erreichte Abstraktionsebenen
Abstraktionsebene
Maschinennähe
Typisierung,
Strukturierung von
Ausdrücken,
Anweisungen,
Daten, Abstraktion
von Algorithmen,
Daten, Verhalten
Component
Pascal
Eiffel
C++
☺ SYSTEM-
unsicher,
Modul
goto
☺
☺
Bibliotheken
Java
☺ unsafeModifikator,
goto
☺
☺
☺
☺
☺
☺
☺ Delegate,
Objektorientierte
Entwurfsmuster
Rohbauten
C#
Iterator
☺ BlackBox
☺ .NET
Tabelle 1.7 Unterscheidende Sprachmerkmale
Sprachmerkmal
Vertragsspezifikation,
Prädikatenlogik
Namensräume
Component
Pascal
Eiffel
Zusicherung
Modul,
Subsystem
Automatische
Speicherbereinigung
Ausnahmebehandlung
Getrennte Übersetzung
Prozeduren
Metaprogrammierung
27.9.12
☺
☺
silent trap
Modul
Zusicherung
Zusicherung
nein
nein
☺ Meta-
Zusicherung
☺
nein
☺
C#
☺
Aufzählungstypen
Überladung
Java
☺
Mehrfaches Erben
Schachtelung
☺
C++
☺
☺
☺
package
☺
interface
interface
Klassen
Klassen
Klassen
☺
☺
☺
Funktionen,
Operatoren
Methoden
Methoden
☺
☺
☺
☺
☺
☺
☺ reflection,
☺ lambda
☺
Vorübersetzer
☺ templates
annotations
expressions,
method attrib.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 32
1.8.5
1 Einführung
Sprachdefinitionen und Syntaxbeschreibungen
zu ergänzen
1.8.6
Sprachimplementationen
Component Pascal
Obwohl der Umfang von Component Pascal relativ klein ist, enthält es genügend Konstrukte, um Programme für einen weiten Anwendungsbereich zu schreiben. Ein Satz
von Standardprozeduren ist Bestandteil der Sprachdefinition. Ein-/Ausgabe ist nur mit
Standardbibliotheken möglich.
Der BlackBox Component Builder der schweizer Firma Oberon microsystems Inc. ist
eine integrierte Entwicklungsumgebung mit Browser, Editor, Kompilierer, Interpretierer und Lader. BlackBox soll die Entwicklung wiederverwendbarer Komponenten
unterstützen. Es besteht aus einer Bibliothek (library) von Grundkomponenten, einem
Rohbau (framework) namens BlackBox Component Framework, d.h. einer erweiterbaren Ansammlung wiederverwendbarer Schnittstellen, und einer Menge direkt benutzbarer Standardkomponenten, die diesen Rohbau implementieren oder erweitern. Entwickler können BlackBox durch zusätzliche Module in existierenden Subsystemen oder
durch zusätzliche Subsysteme erweitern.
Eiffel
Nur mit Sprachkonstrukten von Eiffel kann man kein interaktionsfähiges Programm
erstellen, man braucht einen Komplex von Standardklassenbibliotheken dazu. Es gibt
Werkzeuge und Bibliotheken für Eiffel von mehreren Herstellern.
Bertrand Meyers amerikanische Firma Eiffel Software Inc. (früher Interactive Software Engineering, ISE) bietet mit EiffelStudio eine integrierte Multiplattform-Entwicklungsumgebung in freien und kommerziellen Varianten, und mit EiffelEnvision
ein Plug-In für Visual Studio .NET.
Die deutsche Firma Object Tools GmbH liefert mit Visual Eiffel eine integrierte
Windows-Intel-Entwicklungsumgebung in freien und kommerziellen Varianten.
SmartEiffel (früher SmallEiffel), ein GNU-Open-Source-Projekt des InformatikForschungszentrums LORIA in Nancy, umfasst Kompilierer, Debugger, Bibliotheken und verschiedene Werkzeuge für Windows, Linux und Unix.
Für das Informatik 3 Praktikum empfiehlt sich, die freie Windows-Variante von EiffelStudio zu benutzen.
C++
C++ enthält viele Operatoren, aber nur mit Sprachkonstrukten kann man kein arbeitsfähiges Programm schreiben. Die Sprache entfaltet ihre Stärken zusammen mit den
zugehörigen Vorübersetzerdirektiven und umfangreichen Klassenbibliotheken. Ein-/
Ausgabe ist nur mit Standardbibliotheken möglich.
Es gibt unzählige Werkzeuge und Bibliotheken für C++. Für das Informatik 3 Praktikum empfiehlt sich, die freie Windows-Variante von Code::Blocks zu benutzen.
Java
Nur mit Sprachkonstrukten von Java kann man kein interaktionsfähiges Programm
erstellen, man braucht einen Komplex von Standardklassenbibliotheken dazu. Werkzeuge und Bibliotheken für Java gibt es von mehreren Herstellern, vor allem aber von
Sun Microsystems, seit 2010 Oracle Corporation.
C#
Nur mit Sprachkonstrukten von C# kann man kein interaktionsfähiges Programm
erstellen, man braucht einen Komplex von Standardklassenbibliotheken dazu. Werkzeuge und Bibliotheken für C# gibt es vor allem von Microsoft, insbesondere die integrierte Entwicklungsumgebung Visual Studio .NET.
Für das Informatik 3 Praktikum empfiehlt sich, die freie Windows-Variante von
SharpDevelop zu benutzen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
1.8 Überblick über die Auswahlsprachen
1.8.7
1 – 33
Zielsprachen und Übersetzungskonzepte
Höhere Programmiersprachen werden als Quellsprachen in Zielsprachen übersetzt.
Konzepte des Übersetzens sind Kompilation und Interpretation, die sich kombinieren lassen. Zielsprache und Übersetzungskonzept sind keine Eigenschaften der Quellsprache, sondern Eigenschaften des Übersetzers oder der Entwicklungsumgebung.
Eine Quellsprache lässt sich zumindest prinzipiell mit verschiedenen Übersetzungskonzepten in relativ beliebige Zielsprachen übersetzen. Das Folgende geht auf die
Übersetzungskonzepte ein, die einige Übersetzer und Entwicklungsumgebungen der
fünf betrachteten Sprachen einsetzen.
Entwurfsaspekte
Component Pascal
Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen:
In welche Zielsprache soll übersetzt werden?
Soll kompiliert oder interpretiert werden?
Der BlackBox-Übersetzer kompiliert Component Pascal direkt in Maschinensprache
(native code). Der Kompilierer ist schnell; er übersetzt die rund 240 Module von BlackBox selbst in wenigen Augenblicken. Die Sprache ist so entworfen, dass ein SinglePass-Compiler mit einfachem Top-Down-Parser mit rekursivem Abstieg sie übersetzen
kann. Der erzeugte Maschinencode ist hinsichtlich Effizienz mit laufzeitoptimiertem
C-Code vergleichbar.
Es gibt Kompilierer, die Component Pascal in Java-Bytecode übersetzen.
Qualität von
Kompilierern
Exkurs. Viele Kompilierer verwenden „Optimierungen“, um effizienteren Code zu erzeugen.
Solche Optimierungen erhöhen aber die Komplexität und den Umfang eines Kompilierers und
verlängern die Kompilationszeiten. Niklaus Wirth verwendete eine intuitive Metrik, um ein
günstiges Verhältnis zwischen der Effizienz des Kompilierers und der Effizienz des erzeugten
Codes zu finden [Fra00]. Der Kompilierer ist in der Quellsprache geschrieben, die er übersetzt.
(Man erreicht dies durch ein Bootstrap-Verfahren.) Das Maß für die Qualität des Kompilierers
ist seine Selbstkompilationszeit: die Zeit, die er braucht, um sich selbst zu übersetzen. Eine
Optimierung wird nur in den Kompilierer aufgenommen, wenn sie seine Selbstkompilationszeit
reduziert, sonst lohnt sich der Aufwand nicht. Wegen dieser Vorgehensweise haben Wirths
Kompilierer keine separaten Optimierungsphasen.
Man kann mit derselben Metrik den Nutzen mancher Sprachkonzepte prüfen, da ein Kompilierer ein hinreichend komplexes Programm ist. Führt der Einsatz des Sprachkonstrukts im Kompilierer nicht zu einer kürzeren Selbstkompilationszeit, so ist sein Nutzen fraglich. Diese Metrik
führt gleichzeitig zu einfachen Sprachentwürfen, schnellen Kompilierern und effizientem
Objektcode.
Eiffel
EiffelStudio von Eiffel Software kompiliert zweistufig: Erst wird Eiffel in C, dann C
mit einem gängigen C-Kompilierer in Assembler- oder Maschinensprache übersetzt.
Dieses Konzept ist inzwischen auch bei anderen Sprachen verbreitete Praxis. Der Vorteil ist die Portierbarkeit des Eiffel-Systems: Da C auf vielen Plattformen bereitsteht, ist
Eiffel auf diese Plattformen portierbar. EiffelStudio verwendet C also als maschinenunabhängigen Luxusassembler, um Portierbarkeit zu erzielen. Ein Nachteil dabei ist
langsame Übersetzung, die sich besonders bei großen Programmsystemen mit vielen
Nachkompilationen negativ auf die Produktivität auswirkt. Um diesem Nachteil entgegenzuwirken, entwickelte Eiffel Software die so genannte Melting-Technik. Dabei werden kleine Änderungen an Eiffel-Quellcode mit schon erzeugtem C-Code verschmolzen und interpretiert, ohne vollständig in Maschinencode übersetzt zu werden. Der vom
Eiffel-Kompilierer erzeugte C-Code ist für Menschen kaum lesbar und nicht für manuelle Änderungen gedacht.
EiffelEnvision von Eiffel Software kompiliert in die Common Intermediate Language
(CIL) der .NET-Umgebung. Visual Eiffel übersetzt direkt in die Maschinensprache der
Intel-X86-Prozessoren. SmartEiffel übersetzt wahlweise in C oder Java-Bytecode.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
1 – 34
1 Einführung
C++
Als C++ noch C with Classes hieß, entwickelte Stroustrup für seine Spracherweiterungen den Vorübersetzer cfront, der sie in C übersetzte. Der Vorübersetzer wuchs und verbreitete sich mit C++. Später wurden unabhängig voneinander mehrere Kompilierer
entwickelt, die C++ in Assembler- oder Maschinensprache übersetzen.
Java
Die Übersetzung von Java erfolgt zweistufig. Dem Slogan „write once, run everywhere“ entsprechend wird Java üblicherweise in eine abstrakte Maschinensprache, den
Bytecode kompiliert, der dann durch eine Java Virtual Machine (JVM) interpretiert
wird. Eine JVM kann in einen Web-Browser integriert sein, um Java-Applets zu interpretieren. An einem Applet beteiligte Klassen werden dynamisch über das Internet
geladen.
Der Vorteil der Maschinenunabhängigkeit ist klar. Ein Nachteil ist, dass Interpretation
mindestens eine Größenordnung langsamer als Kompilation ist. Um diesem Nachteil
entgegenzuwirken, verwendet man heute auch Just-In-Time-Compiler (JIT), die den
Bytecode unmittelbar vor der Ausführung kompilieren.
Inzwischen werden auch andere Sprachen in Java-Bytecode kompiliert.
Historisches
Exkurs. Die Ideen des Bytecodes und der virtuellen Maschine sind nicht neu. Niklaus Wirth
benutzte schon 1966 im Kompilierer zu seiner ersten Sprache Euler einen Interpretierer als
Back-End. Um Portierungen von Pascal zu erleichtern, lieferte Wirth Anfang der 1970er Jahre
eine freie Variante des Pascal-Kompilierers, der einen abstrakten Maschinencode namens PCode erzeugte. Eine virtuelle Maschine namens Pascal-P interpretierte den P-Code. Pascal-PImplementationen standen bald auf vielen Rechnern bereit. Der Java-Bytecode ähnelt dem PCode, die JVM dem Pascal-P-Interpretierer.
C#
Auch die Übersetzung von C# erfolgt zweistufig. Ähnlich wie Java wird C# in einen
abstrakten Maschinencode kompiliert, der firmenintern Microsoft Intermediate Language (MSIL) und im ECMA-Standard Common Intermediate Language (CIL) heißt.
Für die Übersetzung von MSIL stehen drei verschiedene JIT-Compiler zur Verfügung.
Im Unterschied zu Java, das eine Sprache für alle Plattformen sein soll, fokussiert
Microsoft weniger auf Maschinenunabhängigkeit als auf Interoperabilität vieler Sprachen auf einer gemeinsamen Plattform: Ada, APL, Beta, C, C++, C#, Cobol, Component Pascal, Eiffel, F#, Forth, Fortran, Haskell, Java, JavaScript, Lisp, Logo, Modula2, Oberon, Pascal, Perl, PHP, Prolog, Python, Ruby, Scala, Scheme, Smalltalk, Spec#,
Standard MetaLanguage, Tcl/Tk, Visual Basic.NET und viele andere Sprachen werden
auf .NET alle in MSIL kompiliert und lassen sich so beliebig kombinieren.1
1
Brian Ritchie’s .NET Development Site: dotnetpowered Language List. http://dotnetpowered.com/languages.aspx; .NET Languages: Resources .NET Language Sites. http://www.dotnetlanguages.net/DNL/Resources.aspx (Zugriffe 2012-03-08).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2
Einmal quer durch die Sprachen
Aufgabe
Beispiel
2
Bild
2 2 wird
(2) endgen
Leitlinienimmermehr,
Programm
2
2
O, dieser
Streit
Tabelle 2
Stets wird die Wahrheit hadern mit dem Schönen,
Stets wird geschieden sein der Menschheit Heer
In zwei Partein: Barbaren und Hellenen.
Heinrich Heine (1797 – 1856)
Es träumte mir von einer Sommernacht
Dieses Kapitel führt mit kleinen Beispielprogrammen in die Auswahlsprachen Component Pascal, Eiffel, C++, Java und C# ein. Formuliert in strukturiertem, modularem
und objektorientiertem Programmierstil, demonstrieren sie Standardlösungen zu den
Standardproblemen
sequenzielle Textausgabe,
Argumentübernahme von der Kommandozeile,
Konvertierung von Zeichenketten in Zahlen,
Vereinbarungen von Klassen und Objekten.
Wichtige Sprachkonstrukte werden angewandt, aber nicht jedes Detail wird diskutiert.
2.1
Hello-World-Programme
Problem
Wie sagt uns ein laufendes Programm, dass es läuft? Schon die seit Kernighans und
Ritchies C-Buch ([KeR88], [KeR90]) unvermeidlichen zeilenorientierten Hello-WorldProgramme nutzen viele Sprachmerkmale und spiegeln typische Sprachideen. Einer
einfachen Lösung folgt meist eine raffiniertere Variante mit den Aspekten
Dokumentation,
geschützte Konstante,
öffentliche Routine (Oberbegriff für Prozedur und Funktion).
2.1.1
Component Pascal
Component Pascal bietet eine modulare Lösung. Der Quelltext des Moduls I0HelloWorld
wird in der Datei I0/Mod/HelloWorld.odc gespeichert.
Programm 2.1
Component Pascal:
Hello World
MODULE I0HelloWorld;
IMPORT
Out;
PROCEDURE Do*;
BEGIN
Out.Open;
Out.String ("Hello World");
Out.Ln;
END Do;
END I0HelloWorld.
Zur Ausgabe importiert I0HelloWorld das nur für Lehrzwecke vorgesehene Standardbibliotheksmodul Out. Die Prozedur Do gehört wegen der Exportmarke „*“ zur Schnittstelle von I0HelloWorld. Der Aufruf des Kommandos I0HelloWorld.Do führt dazu, dass die
Module Out und dann I0HelloWorld dynamisch gebunden und geladen werden.
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 2 – Seite 1 von 42
2–2
2.1.2
2 Einmal quer durch die Sprachen
Eiffel
Eiffel löst das Problem objektorientiert. Der Quelltext der Klasse HELLO_WORLD_1
wird in der Datei HELLO_WORLD_1.e gespeichert. (Auf Unix Klein-Großschreibung
beachten – auch bei anderen Sprachen!)
Programm 2.2
Eiffel: Hello World 1
class HELLO_WORLD_1
create
make
feature
make
do
print ("Hello World%N")
end
end
Der create-Abschnitt deklariert die Prozedur make zu einer Erzeugungsprozedur (creation procedure). Erzeugungsprozeduren heißen konventionell oft make. Nur Erzeugungsprozeduren mit speziellen Signaturen können als Wurzelprozeduren für Programmabläufe dienen.
feature startet einen Abschnitt mit öffentlichen Merkmalen, wovon es hier nur make
gibt. HELLO_WORLD_1 ist wie jede Klasse (außer ANY und GENERAL) implizit Nachfolger der Klasse ANY, die von GENERAL erbt, wo die von make aufgerufene Ausgabeprozedur print definiert ist, d.h. print gehört durch Vererbung automatisch zu
HELLO_WORLD_1. %N erzeugt eine neue Zeile.
Programm 2.3
Eiffel: Hello World 2
note
description: "Simple example for standard output"
author:
"Mrs. Anybody"
history:
"07-03-24, 11-10-06 syntax updated"
class HELLO_WORLD_2
create {NONE}
make
feature {NONE}
Message : STRING = "Hello World"
make
-- Print current object with a simple message in a line.
do
print (Current)
io.put_string (" says: " + Message)
io.put_new_line
end -- make
end -- class HELLO_WORLD_2
Dokumentation
In der raffinierten Variante dient der note-Abschnitt (früher: indexing) der Beschreibung der Klasse, vergleichbar dem meta-Etikett (tag) von HTML. Dokumentationswerkzeuge können note-Abschnitte, Verträge und spezielle Kommentare wie die
Beschreibung von make extrahieren. Das Folgende dokumentiert die Eiffel-Beispiele im
Eiffel-Stil, um professionelle, softwaretechnisch sinnvolle Dokumentation zu zeigen.
Der create-Abschnitt beschränkt die Erzeugungserlaubnis auf die Menge {NONE}, also
die Klasse NONE, deren Schnittstelle leer ist und deren Implementation die Erzeugungserlaubnis nicht nutzt. So kann es von HELLO_WORLD_2 keine Objekte außer dem
Wurzelobjekt geben. Da der Abschnitt der Merkmalsvereinbarungen durch
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.1 Hello-World-Programme
Exportbeschränkung
2–3
feature {NONE}
eingeleitet ist, werden die Merkmale make und Message nur an NONE exportiert. So sind
make und Message vor Kundenzugriffen geschützt. Dies zeigt exemplarisch das Konzept des auf eine spezifizierte Menge von Klassen beschränkten Merkmalsexports.
Als Beispiel für eine Konstantenvereinbarung in Eiffel ist für die interessante Nachricht
das konstante Attribut Message durch
Konstante
Message : STRING = "Hello World"
vereinbart. STRING ist eine Standardbibliotheksklasse. Eine Konstante hat in jedem
Objekt der vereinbarenden Klasse denselben Wert, muss also nicht in jedem Objekt
einen eigenen Speicherplatz belegen, sondern nur einen für alle Objekte der Klasse.
Eiffel überlässt es Compilerbauern, Konstanten speichereffizient zu implementieren.
Was ist in make anders? „--“ startet einen Zeilenendkommentar. print hat die Signatur
Polymorphie
print (some : GENERAL)
mit some als polymorphem Parameter, wodurch print alle Arten von Objekten ausgeben
kann, indem es some.out ausgibt, da jedes Objekt mit der von ANY geerbten Abfrage
out : STRING
eine redefinierbare Standarddarstellung von sich liefert. Entsprechend gibt
print (Current)
das mit dem Standardnamen Current bezeichnete aktuelle Objekt aus, also die Zeichenkette Current.out. make nutzt auch die von ANY geerbte Abfrage io des Typs STD_FILES,
die Zugriffe auf die Standarddateien Eingabe, Ausgabe und Fehler ermöglicht. „+“ ist
zwischen Zeichenkettenoperanden der Konkatenationsoperator. Das verbose
io.put_new_line
vermeidet das kryptische %N.
2.1.3
C++
C++ bietet eine hybride Lösung. Der Quelltext wird in einer beliebig benannten Datei
mit der konventionellen Namenserweiterung cpp gespeichert; hier lautet der Dateiname
HelloWorld1.cpp.
Programm 2.4
C++: Hello World 1
#include <iostream>
int main () {
std::cout << "Hello World\n";
}
Vorübersetzer
Um Ausgabe zu ermöglichen, muss die Schnittstellendatei (header file) iostream.h für
Ein-/Ausgabe-Ströme mit der Vorübersetzerdirektive #include inkludiert werden.
Dadurch wird vor dem Kompilieren der Inhalt der Datei textuell für die Inkludierungsdirektive eingesetzt. Der davon unabhängig zur Implementationsdatei iostream.cpp
erzeugte Objektcode muss zum kompilierten main dazugebunden werden.
Die in iostream.h vereinbarten Größen gehören zum Namensraum (namespace) std. Auf
diese Größen wird mit qualifizierten Namen, hier std::cout, zugegriffen; „::“ ist der
Sichtbarkeitsbereichsauflösungsoperator (scope resolution operator). Namen von
Dateien (iostream) und Namensräumen (std) sind unabhängig voneinander frei wählbar.
cout bezeichnet ein globales Wertobjekt des Typs ostream, den Standardausgabestrom.
(Wo dieses Objekt vereinbart ist und wie es initialisiert wird, bleibt im Dunkeln.) „<<“
ist ein Operator der Klasse ostream, der hier auf cout angewandt wird. (Die Notation
erinnert an die Eingabeumlenkung der Unix-Shell.) Die Zeichenkette ist Parameter des
Operators. \n erzeugt eine neue Zeile. Die Aufrufstelle erhält von main als Ergebnis
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2–4
2 Einmal quer durch die Sprachen
implizit 0 zurück. Der Rückgabewert ist bedeutungslos, offenbar ist die Hauptsache an
der main-Funktion der erzeugte Nebeneffekt.
Programm 2.5
C++: Hello World 2
#include <iostream>
int main (int argc, char * argv[]) {
std::cout << "Hello World" << std::endl;
}
Nebeneffektbehafteter
Ausdruck
Leitlinie 2.1
Vermeide
Nebeneffekte
Diese Variante zeigt die optionale Signatur von main, um Argumente von der Aufrufstelle zu übernehmen; darauf geht 2.2 ein. Die wichtige Lektion ist, dass der von C
geerbte algorithmische Kern von C++ auf dem Konzept nebeneffektbehafteter Ausdrücke beruht. Dazu ist das kryptische \n durch std::endl ersetzt. Mathematische Ausdrücke
liefern Werte, bewirken aber keine Nebeneffekte. Dagegen ist ein Ausdruck als Anweisung einer imperativen Programmiersprache nur sinnvoll, wenn er einen Nebeneffekt
bewirkt und der gelieferte Wert überflüssig ist. Dieses Konzept ist einerseits ausdrucksstark, andererseits wegen der Nebeneffekte schwer verständlich, fehlerträchtig und
daher softwaretechnisch problematisch.
Nebeneffekte behindern systematisches Entwickeln zuverlässiger Software stark. Da
die C*-Sprachen die Konzepte der Ausdrucksanweisung und der nebeneffektbehafteten Ausdrücke und Operatoren enthalten, kann man mit ihnen zwar nicht nebeneffektfrei programmieren, sich aber auf sprachbedingte Nebeneffekte beschränken:
Vermeide Nebeneffekte, wo sie sich nicht durch Sprachkonstrukte umgehen oder
softwaretechnisch gut begründen lassen!
Die Ausdrucksanweisung (expression statement)
Ausdrucksanweisung
std::cout << "Hello World" << std::endl;
besteht aus fünf Ausdrücken, von denen zwei Nebeneffekte bewirken. Um das zu
erkennen, schreiben wir die Anweisung um in die äquivalente Punktnotation, wobei
wir die Qualifikation std:: vereinfachend weglassen. cout ist ein nebeneffektfreier Ausdruck, der ein Objekt bezeichnet. Der Operator << ist an cout gebunden und mit einem
Punkt selektierbar; seine Parameter setzen wir in runde Klammern:
(cout.operator<<("Hello World")).operator<<(endl);
Der aktuelle Parameter "Hello World" ist ein konstanter, nebeneffektfreier Ausdruck.
Der Ausdruck
cout.operator<<("Hello World")
gibt als Nebeneffekt "Hello World" aus und liefert als Wert eine Referenz auf das Objekt
namens cout. Durch die folgende Selektion mit „.“ wird die Referenz implizit dereferenziert. Auf das referenzierte Objekt cout wird wieder << angewandt, diesmal mit dem
nebeneffektfreien Ausdruck endl als Parameter. Der Ausdruck
cout.operator<<(endl)
bewirkt als Nebeneffekt eine neue Ausgabezeile und liefert als Wert eine Referenz auf
cout. Da der Ausdruck als Anweisung mit „;“ abgeschlossen ist, wird der Wert nicht
benötigt und weggeworfen.
Historisches
Exkurs. Die Idee der Ausdrucksanweisung stammt von Niklaus Wirth, der sie 1966 mit seiner
ersten Programmiersprache Euler veröffentlichte [WiW66a], [WiW66b]. Während Wirth dieses
Konzept bald verwarf und schon in seinen nächsten Sprachen Algol-W und Pascal die saubere
Trennung zwischen Ausdrücken und Anweisungen bevorzugte, fand die Ausdrucksanweisung
über Algol 68 Eingang in C, von dem aus sie sich fortpflanzte [Lin96] S. 42. Algol 68 machte
sogar aus jeder Anweisung einen Ausdruck! Sprachen, die die Konzepte Anweisung und Ausdruck vermischen, nennt man ausdrucksorientiert. Dazu schreibt Wirth [Wir05]:
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.1 Hello-World-Programme
2–5
„The heart of the trouble lies in the elimination of the fundamental distinction between statement and expression. The former is an instruction, and it is executed; the latter stands for a
value to be computed, and it is evaluated.“
Überladener Operator
Zweitens beruht die Ausdrucksanweisung auf dem in C++ beliebten Überladen (overloading) von Operatoren. Bei Aufrufen überladener Funktionen und Operatoren wählt
der Kompilierer die passenden Implementationen statisch anhand der Typen der aktuellen Parameter. Von << werden hier zwei Varianten aufgerufen: Die erste Variante hat
einen formalen Parameter des Typs „konstanter Zeiger auf Zeichen“, die zweite einen
des Typs „Zeiger auf eine Funktion, die einen Parameter des Typs Referenz auf
basic_ostream hat und deren Rückgabetyp auch Referenz auf basic_ostream ist“. Der
aktuelle Parameter "Hello World" ist eine Zeichenkette und passt zur ersten Variante.
Der aktuelle Parameter endl – ein Manipulator – ist eine Funktion mit zur zweiten Variante passender Signatur. Die zum Aufruf
cout.operator<<(endl)
gewählte Implementation von << ruft die übergebene Funktion endl so auf:
endl(cout)
Dieser Aufruf in pseudofunktionalem Stil führt zum Aufruf
cout.endl()
der Methode endl(), die zu ostream gehört und hier am Objekt cout angewandt wird. Die
Kombination verschiedener Konzepte erlaubt es also, kurz
cout << "Hello World" << endl;
statt etwa
cout.print("Hello World"); cout.endl();
oder
cout.printline("Hello World");
zu schreiben. Das einfache Beispiel zeigt, wie die Ausdrucksstärke von C++ die
Schreibbarkeit (nicht die Problemlösungsmächtigkeit) erhöht, aber auch zu schwer
durchschaubarem Quelltext führt.
Historisches
Exkurs. Das Überladen von Operatoren ist keine Innovation von C++, sondern aus Algol 68
und Ada 83 bekannt.
Da die C++-Welt viele, aber keine einheitlichen Dokumentationsregeln kennt, verzichtet dieser Text auf eine dokumentierte Variante. Um nach den prozeduralen Varianten
etwas Objektorientierung ins Spiel zu bringen, folgen eine modulare und eine objektorientierte Variante. Jede definiert eine Klasse, die von der unvermeidlichen mainFunktion benutzt wird. In Programm 2.6 fungiert die Klasse als Modul, sie hat kein
Datenelement, eine expandierte Klassenfunktion und wird nicht getrennt übersetzt. In
Programm 2.7 hat die Klasse ein konstantes Datenelement, eine nicht expandierte Elementfunktion, wird getrennt übersetzt und durch ein lokales Wertobjekt benutzt. Die
verschiedenen Aspekte lassen sich beliebig mit weiteren Aspekten kombinieren.
public
Programm 2.6 enthält die Definition der Klasse HelloWorld, die als einziges Element die
Funktion act besitzt (so genannt, weil do ein reserviertes Wort ist). Da act in einem
public-Abschnitt definiert ist, gehört act zur Schnittstelle der Klasse.
static
Der Spezifikator static macht act zu einer Klassenfunktion, d.h. einer Funktion, die an
die Klasse, nicht an einzelne Objekte gebunden ist. Das Schlüsselwort static weist hier
nicht auf etwas Statisches hin, sondern wurde vom Sprachentwerfer gewählt, damit er
kein weiteres Schlüsselwort einführen musste. Klassen, die nur mit static spezifizierte
Elemente enthalten, entsprechen Singletons oder Modulen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2–6
Programm 2.6
C++: Hello World 3 mit
Klasse
2 Einmal quer durch die Sprachen
#include <iostream>
class HelloWorld {
public:
static void act () {
std::cout << "Hello World" << std::endl;
}
};
int main () {
HelloWorld::act();
}
void
void bezeichnet einen wertelosen Nichtstyp, der hier als Ergebnistyp von act angegeben
ist, weil act nichts zurückgibt, also eigentlich keine Funktion, sondern eine Prozedur
ist. Die C*-Welt kennt nur Funktionen, Prozeduren sind als void-Funktionen zu definieren. Das leere Klammernpaar () zeigt an, dass die Formalparameterliste von act leer ist.
In den C*-Sprachen sind void und () von C geerbter syntaktischer Ballast, da act statt
void act () genügen würde, um das Notwendige auszudrücken.
Historisches
Exkurs. C hat void von Algol 68 übernommen, das zudem die Schlüsselwörter empty, nil, skip
und den Begriff undefined kennt, um die Abwesenheit von etwas auszudrücken. „Too much of
nothing makes a man feel ill“, kommentiert Bob Dylan.
Inlining
Zwischen den geschweiften Klammern steht der Anweisungsteil von act. Da er innerhalb der Klassendefinition steht, handelt es sich um eine Inline-Funktion, d.h. der
Kompilierer kann den Anweisungsteil expandieren, d.h. an Aufrufstellen einsetzen
und so die Routinensprungbefehle sparen. Als zu einer void-Funktion gehörend braucht
der Anweisungsteil von act keine return-Anweisung zu enthalten.
Im Anweisungsteil von main wird das act von HelloWorld aufgerufen. Da act eine Klassenfunktion ist, wird der Aufruf mit dem Klassennamen und dem Sichtbarkeitsbereichsauflösungsoperator :: qualifiziert. Die Klasse wird wie ein Modul verwendet.
Die Auswertung des void-Ausdrucks
HelloWorld::act()
bewirkt den gewünschten Effekt als Nebeneffekt und liefert nichts zurück.
Wiederverwendbarkeit
Klassen sollen wiederverwendbar sein. Damit eine Klasse ohne Replikation wiederverwendbar ist, muss sie getrennt übersetzbar sein. Doch C/C++ kennen nur unabhängiges Übersetzen; getrenntes Übersetzen simulieren sie mit einem Vorübersetzer auf dem
Technikstand der frühen 1970er Jahre. Entsprechend muss der C++-Programmierer
den Quelltext einer Klasse in einen Schnittstellen- und (mindestens) einen Implementationsteil trennen und auf (mindestens) zwei Dateien verteilen. Für main ist eine dritte
Datei vorzusehen.
Bild 2.1 Inkludierungsbeziehungen zu Programm 2.7
HelloWorld.hpp
HelloWorld4.cpp
Inkludierung
string.h
iostream.h
HelloWorld.cpp
Bild 2.1 zeigt, dass in diesem einfachen Beispiel keine Schnittstellendatei mehrfach in
eine Implementationsdatei inkludiert wird. Bei realen Anwendungen passiert es aber
leicht, dass Schnittstellendateien indirekt über Inkludierungsketten mehrfach inkludiert
werden. Die mehrfach vorkommenden Bezeichner führen dann beim Kompilieren zu
Kontextfehlern mit der Meldung „duplicate symbol“.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.1 Hello-World-Programme
Programm 2.7
C++: Hello World 4 mit
wiederverwendbarer
Klasse
2–7
// Header file: HelloWorld.hpp
#ifndef HELLO_WORLD_HPP_
#define HELLO_WORLD_HPP_
#include <string>
class HelloWorld {
protected:
static const std::string message;
public:
void act ();
};
const std::string message = std::string("Hello World");
#endif // HELLO_WORLD_HPP_
// Implementation file: HelloWorld.cpp
#include "HelloWorld.hpp"
#include <iostream>
void HelloWorld::act () {
std::cout << message << std::endl;
}
// Implementation file: HelloWorld4.cpp
#include "HelloWorld.hpp"
int main () {
HelloWorld hello;
hello.act();
}
Um das Problem zu vermeiden, gewöhnt sich der professionelle Programmierer an die
Konvention, die Inhalte von Schnittstellendateien mit ifndef-define-endif-Vorübersetzerdirektiven zu klammern. Dazu ist zu jeder Schnittstellendatei ein systemweit eindeutiges Symbol zu wählen, das z.B. konventionell aus ihrem Namen so abgeleitet ist: Alle
Buchstaben groß, zwischen Teilwörtern, für den Punkt und am Ende ein Unterstrich.
Die Direktive
#ifndef X_HPP_
prüft, ob das Symbol X_HPP_ definiert ist; falls nicht, wird der folgende Text bis zur
Direktive
#endif
inkludiert, sonst nicht. Falls also inkludiert wird, definiert die Direktive
#define X_HPP_
das zuvor undefinierte Symbol, was ein zweites Inkludieren derselben Datei verhindert.
Leitlinie 2.2
C++: Inkludiere nur
bedingt
Klammere den C++-Quelltext in der Schnittstellendatei x.hpp mit den Vorübersetzerdirektiven
#ifndef X_HPP_
#define X_HPP_
C++-Quelltext
#endif // X_HPP_
um mehrfaches Inkludieren zu vermeiden!
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2–8
Schnittstellendatei
Exportbeschränkung
Konstante
2 Einmal quer durch die Sprachen
Die Schnittstellendatei HelloWorld.hpp enthält die Definition der Klasse HelloWorld.
Dazu gehört nicht nur die Kundenschnittstelle der Klasse, sondern auch ein Teil ihrer
Implementation, nämlich hier das durch protected geschützte Datenelement message.
Wegen protected dürfen nur von HelloWorld abgeleitete Klassen auf message zugreifen.
message ist in der Klassendefinition von HelloWorld als Datenelement des Typs
std::string, einer in string.h definierten Standardbibliotheksklasse, deklariert durch
Deklaration
static const std::string message;
Der Spezifikator const macht message zu einem konstanten Datenelement. Der Spezifikator static macht message zu einem Klassendatenelement, damit es nicht in jedem
Objekt eigenen Speicherplatz belegt. const ist nur kombiniert mit static sinnvoll, doch
ist die Kombination nicht zwingend; der Programmierer ist also gefordert, diesen Hinweis auf eine effiziente Implementation hinzuschreiben. Das Beispiel zeigt, wie Orthogonalität mit Effizienz konfligieren kann.
Leitlinie 2.3
C++: Vermeide
replizierte Konstanten
Konstante Datenelemente in Klassen sollen Klassendatenelemente sein. Verwende
also in Klassendefinitionen const bei Datenelementen nur mit static!
message muss außerhalb der Klasse in einer Definition initialisiert werden, wofür C++
die drei äquivalenten Notationen
Definition
const std::string message = std::string("Hello World");
const std::string message ("Hello World");
const std::string message = "Hello World";
bietet, die zu einem Aufruf eines passenden Konstruktors der string-Klasse führen, der
message mit der interessanten Nachricht initialisiert.
Implementationsdatei
Die Implementationsdateien HelloWorld.cpp und HelloWorld4.cpp sind unabhängig voneinander zu übersetzen, die erzeugten Objektdateien sind danach zusammenzubinden.
Entwicklungsumgebungen kombinieren Übersetzungs- und Bindeschritte, fordern dazu
aber meist die Definition eines Projekts zum Verwalten der Übersetzungseinheiten.
HelloWorld.cpp enthält die Implementation der Elementfunktion act der Klasse HelloWorld. Um den Bezug zu ihrer Klasse herzustellen, wird ihr Name mit dem Klassennamen und :: qualifiziert. act benutzt message als Argument für den Ausgabeoperator <<,
von dem der Kompilierer die geeignet überladene Variante wählt.
HelloWorld4.cpp enthält die main-Funktion, die ein lokales Wertobjekt namens hello der
Klasse HelloWorld vereinbart. Daraus ist zu erfahren, dass in C* lokale Vereinbarungen
nicht syntaktisch vom Anweisungsteil getrennt, sondern Teil desselben sind.
2.1.4
Java
Java bietet eine objektorientierte Lösung, die syntaktisch C++ ähnelt. Der Quelltext
der Klasse HelloWorld1 wird in der Datei HelloWorld1.java gespeichert. Die
erzeugte Bytecode-Datei heißt HelloWorld1.class.
Programm 2.8
Java: Hello World 1
public class HelloWorld1 {
public static void main (String[] args) {
System.out.println("Hello World");
}
}
Beim Starten der HelloWorld1-Klasse als Anwendung wird die main-Methode ausgeführt. main ist wegen des Modifikators public öffentlich und wegen static eine Klassenmethode, d.h. sie gehört zur Klasse, nicht zu den Objekten. Ihr Ergebnistyp ist
void, weil sie nichts zurückgibt. Wie bei C++ übernimmt main Argumente von der
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.1 Hello-World-Programme
2–9
Aufrufstelle, hier aber obligatorisch. String ist eine Standardbibliotheksklasse, []
markiert eine offene Reihung.
Die Ausgabemethode println gehört zum Objekt out, dessen Klasse PrintStream ist.
out ist eine Klassenvariable der Klasse System. Von System können keine Objekte
erzeugt werden, diese Klasse existiert nur in der Rolle eines Singletons oder Moduls.
Java vermeidet die trickreiche Ausgabe von C++ zugunsten einer klaren prozeduralen,
nebeneffekt- und überladungsfreien Lösung wie in Component Pascal und Eiffel.
Programm 2.9
Java: Hello World 2
/** Simple example for standard output.
* @author
Mrs. Anybody
* @version
04-09-09
*/
public class HelloWorld2 {
private static final String MESSAGE = "Hello World";
/**
* Prints a simple message in a line.
*/
public static void main (String[] args) {
System.out.println(MESSAGE);
}
}
Dokumentation
Die zweite Variante, Programm 2.9, benutzt dem note-Abschnitt von Eiffel entsprechende Javadoc-Etikette zur Beschreibung der Klasse. Javadoc ist ein Werkzeug, das
speziell etikettierte Kommentare in Dokumentationsdateien extrahieren kann. Professionelle Programmierer sollten alle Java-Programme mit Javadoc-Etiketten nach Konventionen dokumentieren. Es unterbleibt im Folgenden nur, um Platz zu sparen und
weil die Lesbarkeit von Javadoc-Kommentaren zu wünschen übrig lässt.
Für die interessante Nachricht ist das konstante Feld MESSAGE durch
Private Konstante
private static final String MESSAGE = "Hello World";
vereinbart – ein Beispiel für eine Konstantenvereinbarung in Java. Durch den Modifikator private ist MESSAGE nur in HelloWorld2 selbst zugreifbar, also vor Zugriffen
von Kundenklassen (aber auch Erweiterungsklassen) geschützt. Der Modifikator final
macht MESSAGE zusammen mit dem Initialisierungswert "Hello World" konstant. Der
Modifikator static macht MESSAGE zu einer Klassenvariablen, damit es nicht in jedem
Objekt eigenen Speicherplatz belegt. final ist nur kombiniert mit static sinnvoll,
doch ist die Kombination nicht zwingend; wie bei C++ muss der Programmierer diesen Implementationshinweis hinschreiben.
Leitlinie 2.4
Java: Vermeide
replizierte Konstanten
Applet
Konstante Felder sollen Klassenfelder sein. Verwende also final bei Feldern nur
mit static!
Java führte die Idee ein, kleine Programme in Web-Seiten zu integrieren. Solche Programme heißen Applets; es handelt sich um Klassen, die die Standardbibliotheksklasse
java.applet.Applet erweitern. Sie werden auf Web-Servern gespeichert und von
Web-Browsern dynamisch geladen und ausgeführt (direkt oder über Java-Plug-Ins).
Die Beispiel-Applet-Klasse HelloWorld3 (Programm 2.10) soll den Gruß in ein buntes
Rechteck zeichnen. Durch die extends-Klausel erbt sie alle Merkmale von
java.applet.Applet.
Grafik
27.9.12
Die Größe des farbigen Rechtecks legen die Konstanten WIDTH und HEIGHT des Typs
int fest. Die öffentliche Methode paint ist leer von java.applet.Applet geerbt.
paint wird vom Web-Browser implizit aufgerufen. Für einen auf der Web-Seite sichtbaren Effekt ist paint zu überschreiben. Der Parameter g ist von der Klasse
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 10
2 Einmal quer durch die Sprachen
java.awt.Graphics, die Grafikmethoden bietet. Im Einzelnen legt paint hier die Hintergrundfarbe java.awt.Color.green fest, füllt damit das links oben justierte Rechteck der Größe WIDTH * HEIGHT aus, legt die Schriftfarbe java.awt.Color.red fest,
und zeichnet den Text "Hello World" in das Rechteck, beginnend in der Mitte.
Programm 2.10
Java: Hello World 3
public class HelloWorld3 extends java.applet.Applet {
private static final int WIDTH = 300, HEIGHT = 200;
public void paint (java.awt.Graphics g) {
g.setColor(java.awt.Color.green);
g.fillRect(0, 0, WIDTH, HEIGHT);
g.setColor(java.awt.Color.red);
g.drawString("Hello World", WIDTH / 2, HEIGHT / 2);
}
}
Das Applet wird in einem HTML-Text (Programm 2.11) durch das applet-Etikett spezifiziert, wobei ihm ähnlich wie einem Bild ein Rechteck zur Ausgabe zugeordnet wird
(das größer als das im Applet spezifizierte farbige Rechteck sein sollte).
Programm 2.11
HTML: Applet-Aufruf
HelloWorld3
<html>
<head>
<title>Simple Java-Applet</title>
</head>
<body>
Vor dem Applet.
<p align="center">
<applet code="HelloWorld3.class" alt="HelloWorld3-Applet"
width="600" height="300">
Hier stehen Parameter für das Applet.
</applet>
<p>
Nach dem Applet.
</body>
</html>
2.1.5
C#
C# bietet eine objektorientierte Lösung, die sich nur geringfügig von der Java-Variante
unterscheidet. Der Quelltext der Klasse HelloWorld1 wird in der Datei
HelloWorld1.cs gespeichert.
Programm 2.12
C#: Hello World 1
public class HelloWorld1 {
public static void Main () {
System.Console.WriteLine("Hello World");
}
}
Die Main-Methode darf parameterlos sein, kann aber auch wie bei Java Argumente von
der Aufrufstelle übernehmen. System ist nicht wie in Java eine Klasse, sondern ein
Namensraum. Die Klasse oder das Singleton-Objekt System.Console enthält die Klassenmethode WriteLine, die eine Zeichenkette auf die Konsole ausgibt.
Dokumentation
Die zweite Variante benutzt dem note-Abschnitt von Eiffel entsprechende empfohlene
XML-Abschnitte zur Beschreibung der Klasse. In Visual Studio .NET erzeugt ein Werkzeug daraus eine XML-Datei mit Dokumentationskommentaren. Analog zu Java sollten
Profi-Programmierer alle C#-Programme mit XML-Abschnitten nach Konventionen
dokumentieren. Ich ignoriere die Regel, weil ich mit Platz geize, XML-Abschnitte noch
schlechter als Javadoc-Kommentare lesen kann und nicht einsehe, weshalb ich Zeit mit
Tippen von XML-Etiketten verbringen sollte.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.1 Hello-World-Programme
Programm 2.13
C#: Hello World 2
2 – 11
/// <summary>
///
Simple example for standard output.
/// </summary>
/// <remarks>
///
author: Mrs. Anybody;
///
history: 04-09-09
/// </remarks>
public class HelloWorld2 {
protected const string Message = "Hello World";
/// <summary>
///
Prints a simple message in a line.
/// </summary>
public static void Main (string[] args) {
System.Console.WriteLine(Message);
}
}
Für die interessante Nachricht ist die Konstante Message durch
Geschützte Konstante
protected const string Message = "Hello World";
vereinbart – ein Beispiel für eine Konstantenvereinbarung in C#. Der Modifikator protected exportiert Message (wie in C++, aber anders als in Java!) nur an Erweiterungsklassen von HelloWorld2, schützt es also vor Zugriffen von Kundenklassen. string ist
ein Aliasname für die Standardbibliotheksklasse System.String. Der Modifikator
const markiert Konstanten. Da an Objekte gebundene Konstanten Speicherplatz verschwenden, bindet C# Konstanten stets an die Klasse; der Modifikator static ist bei
Konstanten weder nötig noch erlaubt. Die Entwerfer von C# haben sich hier also für
Effizienz, gegen Orthogonalität entschieden.
2.1.6
Weitere Sprachen
Neben und nach C# hat sich die C*-Familie weiter verzweigt. Auf die Hello-WorldProgramme der drei Sprößlinge D, Scala und Go werfen wir einen Blick.
Programm 2.14
D: Hello World
import std.stdio;
void main(string[] args) {
writefln("Hello world");
}
std.stdio bezeichnet das Modul stdio im Paket std. Der Programmablauf beginnt
bei main. Der Ergebnistyp von main kann auch int sein, seine Argumente können ent-
fallen. Noch Fragen? Siehe [D09].
Programm 2.15
Scala: Hello World 1
object HelloWorld1 {
def main(args: Array[String]) {
println("Hello world")
}
}
Das Schlüsselwort object macht HelloWorld1 zu einem Einzelobjekt (singleton) oder
Modul. Da es die Methode main enthält, ist es ein Objekt der obersten Ebene (top-level
object). Der Programmablauf beginnt bei main.
Programm 2.16
Scala: Hello World 2
object HelloWorld2 extends Application {
println("Hello world")
}
Es geht auch ohne main: Ist ein Einzelobjekt mit extends Application vereinbart, so
werden alle Anweisungen in diesem Objekt ausgeführt.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 12
Programm 2.17
Scala: Hello World 3
2 Einmal quer durch die Sprachen
println("Hello world")
Es geht auch ohne object: Steht nur die Anweisung in der Datei HelloWorld3.scala,
so ruft die Kommandozeile
scala HelloWorld3.scala
nicht den Scala-Kompilierer, sondern den Scala-Interpretierer auf, der die Anweisung
in HelloWorld3.scala interpretiert. Es geht auch ohne Datei: Die Kommandozeile
scala -e "println(\"Hello world\")"
bewirkt dasselbe. Die Idee, eine Teilsprache einer Hochsprache als interpretierte Kommandosprache zu verwenden, stammt von Niklaus Wirths Oberon-System [ReW94];
siehe dazu auch 2.2.1.1. Mehr zu Scala in [Sca10].
Programm 2.18
Go: Hello World
package main
import "fmt"
func main() {
fmt.Printf("Hello World\n")
}
Der Quelltext gehört zum Paket main. Das Paket fmt wird importiert. Der Programmablauf beginnt bei der Funktion main. Weitere Fragen beantwortet [Go10].
2.1.7
Fazit
Die Hello-World-Programme lassen folgende Sprachideen erkennen:
Einfach modular löst Component Pascal die Aufgabe.
Rein objektorientiert mit Vererbung löst Eiffel die Aufgabe, wobei ein polymorpher
Parameter das zentrale objektorientierte Konzept der Polymorphie nutzt.
Hybride prozedural-modular-objektorientierte Lösungen erscheinen in C++, wobei
nebeneffektbehaftete Operatoren unvermeidlich und das nicht objektorientierte
Konzept des Überladens von Operatoren und Funktionen beliebt ist.
Hybrid modular-objektorientiert lösen Java und C# die Aufgabe, wobei sie sich
syntaktisch an C++ orientieren, aber semantisch auf Nebeneffekte verzichten.
Java bietet mit Applets die interessante Variante, Java-Programme in Web-Browsern auszuführen.
Die Ausgabe erfolgt
textuell formatiert in ein Dokument, das direkt speicherbar und weiterverarbeitbar
ist (Component Pascal/BlackBox),
textuell unformatiert auf die Kommandokonsole in flüchtiger Form, die Speicherung und Weiterverarbeitung nur indirekt relativ aufwändig zulässt (Eiffel, C*),
grafisch in ein Rechteck in einer Web-Seite (Java-Applet).
In allen Sprachen lassen sich Hello-World-Varianten formulieren, die statt sequenzieller Textausgabe Elemente grafischer Benutzungsoberflächen nutzen.
Aufgabe 2.1
Hello World
Lernen Sie mit den Hello-World-Programmen die Entwicklungsumgebungen kennen!
Erweitern Sie Ihre Lösungen spielerisch um Elemente Ihrer Wahl! Suchen Sie im Web
und in Lehrbüchern nach Hello-World-GUI-Varianten!
Sprachen: Alle.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.2 Argumentübernahme von der Kommandozeile
2.2
2 – 13
Argumentübernahme von der Kommandozeile
Beim Aufruf eines Programms Argumente von der Aufrufstelle an das Programm übergeben – dieses Konzept geht auf C und die Unix-Shell zurück. Traditionell wird ein
Programm durch ein Kommando der Kommandosprache des Betriebssystems – kurz:
von der Kommandozeile – aufgerufen:
Kommandozeile
Problem
command options argument argument ...
Der Kommandospracheninterpretierer übergibt die Argumente an das aufgerufene Programm. C++ hat das Sprachkonstrukt für die Übernahme der Kommandoargumente
von C direkt übernommen. Andere Sprachen setzen das Konzept ähnlich um.
Da moderne Anwendungen generell grafische Benutzungsoberflächen bieten, haben
Kommandoargumente und sequenzielle Textausgabe an Bedeutung verloren. Trotzdem
betrachten wir die Realisierungen dieser Konzepte in den Auswahlsprachen, weil sie
bei Beispiel- und Übungsprogrammen oft nützlich sind. Argumente übergeben ist meist
leichter als Eingabeanweisungen oder Dialoge programmieren.
Die Argumente werden als Zeichenketten übergeben. Handelt es sich um Zahlen, so
sind diese programmseitig in Zahlenformate zu konvertieren. Das Folgende untersucht
die Mittel und Programmierschemas, mit denen die Auswahlsprachen die Konversion
durchführen.
2.2.1
Component Pascal
2.2.1.1
Argumentübernahme
Der BlackBox Component Builder umfasst einen Rohbau erweiterbarer Komponenten.
Allgemein ist der Zweck eines Rohbaus, ihn zu erweitern. Speziell BlackBox wird
erweitert, indem man einzelne Module oder Komponenten aus mehreren Modulen hinzufügt. Die Möglichkeit, mit BlackBox Stand-Alone-Programme zu entwickeln, die mit
dem Binder zu ausführbaren Programmen (auf MS-Windows: EXE-Dateien) zusammengebunden werden, bleibt deshalb hier außer Acht. Stattdessen betrachten wir, wie
gewöhnliche Prozeduren beliebiger Module innerhalb von BlackBox von beliebigen
Stellen – z.B. Dokumenten, Menüs, Querverweisen, Dialogen – aus aufgerufen werden
können. An die Stelle des Kommandointerpretierers des Betriebssystems tritt der Kommandointerpretierer von BlackBox. Er akzeptiert Prozeduren mit beliebig vielen Zeichenketten- und Ganzzahlargumenten in beliebiger Reihenfolge.
Programm 2.19
Component Pascal:
Parameterübernahme
MODULE I0ParameterWriter;
IMPORT
StdLog;
PROCEDURE Do* (IN s1, s2 : ARRAY OF CHAR; i1, i2 : INTEGER);
BEGIN
StdLog.Open;
StdLog.String ("s1: " + s1);
StdLog.Ln;
StdLog.String ("s2: " + s2);
StdLog.Ln;
StdLog.String ("i1: "); StdLog.Int (i1); StdLog.Ln;
StdLog.String ("i2: "); StdLog.Int (i2); StdLog.Ln;
END Do;
END I0ParameterWriter.
Programm 2.19 übernimmt vier Argumente vom Kommandoaufruf und gibt sie aus.
Ein Aufruf der Prozedur Do kann beispielsweise so in einem Dokument stehen:
! "I0ParameterWriter.Do ('erste Zeichenkette', 'zweite Zeichenkette', 1, 2)"
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 14
2.2.1.2
2 Einmal quer durch die Sprachen
Konversion
Soll die Prozedur Gleitpunktzahlen erhalten, so sind diese als Zeichenketten einzugeben und prozedurseitig in Gleitpunktzahlen zu konvertieren; dazu ist z.B. die Zeile
StdLog.String ("s1: " + s1); StdLog.Ln;
durch
VAR
r
result
: REAL;
: INTEGER;
Strings.StringToReal (s1, r, result);
IF result = 0 THEN
StdLog.String ("s1 as real: "); StdLog.Real (r); StdLog.Ln;
END;
zu ergänzen oder zu ersetzen. Die Lösung folgt dem prozeduralen Ein-/AusgabeSchema: StringToReal ist eine Prozedur mit einer Zeichenkette als Eingabeparameter
und der konvertierten Zahl als Ausgabeparameter. Was passiert, wenn die Zeichenkette
keine Gleitpunktzahl enthält, sich also nicht sinnvoll konvertieren lässt? Dieser Fall
sollte nicht ignoriert, sondern angezeigt werden. Die Lösung ist ein Ergebnisparameter: Ein zweiter Ausgabeparameter zeigt Erfolg oder Misserfolg bei der Konversion an.
Die Behandlung des Misserfolgs kann ggf. entfallen.
Ein-/Ausgabe-Schema
mit Ergebnisparameter
(* Try to produce target from any source: *)
Module.Compute (source, target, result);
(* Check result to see if target is usable: *)
IF result is good THEN
process target
ELSE
handle bad case
END;
2.2.2
Eiffel
2.2.2.1
Argumentübernahme
Die Eiffel-Implementation von Eiffel Software bietet einen Mechanismus zur Argumentübernahme von der Kommandozeile, der sich am C/C++-Mechanismus orientiert.1 Andere Eiffel-Implementationen realisieren die Argumentübernahme ähnlich.
Die Erzeugungsprozedur einer Wurzelklasse darf einen Parameter des Typs
ARRAY [STRING] haben:
make (arguments : ARRAY [STRING])
Generische Klasse
ARRAY ist eine generische Standardbibliotheksklasse für Reihungen. STRING ist hier als
aktueller generischer Parameter für den Elementtyp der Reihung eingesetzt.
Programm 2.20 übernimmt beliebig viele Argumente von der Kommandozeile und gibt
sie aus. Beim Aufruf von ARGUMENTS_WRITER.make werden die Kommandoargumente an das Zeichenkettenreihungsobjekt übergeben, auf das sich die Referenz arguments bezieht.
Referenzsemantik
Ohne aktuelle Argumente braucht es kein Reihungsobjekt zu geben! Wegen der Referenzsemantik ist zu unterscheiden, ob sich arguments auf nichts (Void) oder ein Objekt
bezieht. Void ist ein von ANY geerbtes Attribut des Typs NONE, von dem es keine
Objekte geben kann; deshalb stellt Void den leeren Bezug dar. Nur falls arguments /=
Void ist die Selektion eines Objektmerkmals wie arguments.lower legal, wobei arguments
implizit dereferenziert wird.
1
http://docs.eiffel.com/ (Zugriff 2005-03-21).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.2 Argumentübernahme von der Kommandozeile
Programm 2.20
Eiffel:
Argumentübernahme
an der Wurzel
2 – 15
note
description: "Output of command arguments in the root class creation procedure"
class ARGUMENTS_WRITER
create
make
feature
make (arguments : ARRAY [STRING])
-- Print arguments passed from the command-line.
local
i : INTEGER
do
if arguments = Void then
io.put_string ("No arguments received.")
io.put_new_line
else
from
i := arguments.lower
until i > arguments.upper loop
io.put_string ("argument ")
io.put_integer (i)
io.put_string (": " + arguments.item (i))
io.put_new_line
i := i + 1
end
end
end -- make
end -- class ARGUMENTS_WRITER
Wird ARGUMENTS_WRITER.make nur als Wurzelprozedur benutzt, so ist der Void-Test
für arguments bei vielen Eiffel-Implementationen verzichtbar, weil sie als erstes Argument stets den Kommandonamen übergeben. Da manche Eiffel-Implementationen aber
den Kommandonamen nicht übergeben, unterstützt die obige Variante die Portabilität.
Allerdings variiert die Anzahl der Argumente je nachdem, ob der Kommandoname
mitgezählt wird oder nicht.
make hat einen mit local eingeleiteten Vereinbarungsteil, in dem die lokale ganzzahlige
Größe i vereinbart ist. Der Anweisungsteil von make enthält im else-Zweig der zweiseitigen Auswahlanweisung (if-then-else-end) eine Schleife:
Bedingungsschleife
from
i := arguments.lower
until i > arguments.upper loop
io.put_string ("argument ")
io.put_integer (i)
io.put_string (": " + arguments.item (i))
io.put_new_line
i := i + 1
end
Eiffel hat als einzige Schleifenart eine kopfgesteuerte Bedingungsschleife mit Initialisierungsteil und Abbruchbedingung der Form
Schleifenmuster
27.9.12
from
Initialisierungen der Schleifenvariablen
until Abbruchbedingung loop
Schleifenrumpf
end
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 16
2 Einmal quer durch die Sprachen
Die obige Schleife dient als Zählschleife; man beachte das explizite Inkrementieren des
Zählers i am Rumpfende.
Reihung
Von der ARRAY-Klasse werden für den minimalen und den maximalen Index und das
Element mit dem Index i die Abfragen
lower : INTEGER
upper : INTEGER
item (i : INTEGER) : G
benutzt. Für den formalen generischen Parameter G ist aktuell STRING eingesetzt. Eiffel
prüft die Gültigkeit von Reihungsindizes optional dynamisch.
2.2.2.2
Konversion
make erhält die Kommandoargumente als Zeichenketten, auch wenn es sich um Zahlen
handelt. Um Zeichenketten in Zahlen zu konvertieren ist die Zeile
io.put_string (": " + arguments.item (i))
durch
Mehrseitige
Auswahlanweisung
io.put_string (": ")
if arguments.item (i).is_integer then
io.put_string ("integer: "); io.put_integer (arguments.item (i).to_integer)
elseif arguments.item (i).is_real then
io.put_string ("real: ");
io.put_real (arguments.item (i).to_real)
else
io.put_string ("no integer, no real: " + arguments.item (i))
end
zu ersetzen. Die mehrseitige Auswahlanweisung if-then-elseif-else funktioniert genau
wie in Component Pascal.
Zeichenkette
Die Problemlösung ist objektorientiert. Von der STRING-Klasse werden zum Konvertieren die Abfragen
is_integer : BOOLEAN
is_real : BOOLEAN
to_integer : INTEGER
to_real : REAL
auf die durch arguments.item (i) referenzierten STRING-Objekte angewandt. Seltsamerweise nennt der Eiffel Library Standard Vintage 95 nur die letzten beiden Abfragen,
obwohl sie die ersten beiden in Vorbedingungen benötigen.1
Die obige Lösung folgt dem objektorientierten Abfrage-Abfrage-Schema, was bedeutet, sich über den Zustand eines Objekts (unten source) durch eine Abfrage (query) zu
informieren und dann, wenn das Objekt in einem zulässigen Zustand ist, weitere
Zustandsinformation (unten target) abzufragen:
Abfrage-AbfrageSchema
-- Get state information from object without precondition:
if source.unconditional_query is good then
-- Get state information from object with valid precondition:
target := source.conditional_query
process target
else
handle bad case
end
Das Abfrage-Abfrage-Schema lässt sich in jeder imperativen Programmiersprache
ohne besondere Sprachkonstrukte anwenden. Sein Vorteil ist Einfachheit, da Miss1
http://www.eiffel-nice.org/standards/nice-2/elks95.pdf (Zugriff 2005-03-21).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.2 Argumentübernahme von der Kommandozeile
2 – 17
erfolge nicht im Nachhinein auftreten können, sondern von vornherein verhindert werden. Das Motto lautet: Lieber vor einem Auftrag fragen, ob dafür alles in Ordnung ist,
als sich danach um Misserfolge kümmern müssen. Die Behandlung des unzulässigen
Auftrags kann ggf. entfallen.
Aufgabe 2.2
Abfrageorientierte
Konversion
Die Konversion in Eiffel nach dem Abfrage-Abfrage-Schema könnte ineffizient
erscheinen, da das Prüfen, ob eine Zeichenkette eine Zahl darstellt, fast denselben Aufwand fordert wie das Konvertieren der Zeichenkette in eine Zahl. Abfrage-AbfrageSchemas in prozeduralem Stil können tatsächlich ineffizient sein. In objektorientiertem
Stil formuliert kann der Nachteil verschwinden. Wie lässt sich die abfrageorientierte
Konversion effizient implementieren?
2.2.2.3
Entfernte Argumentübernahme
Zwar genügt die Argumentübernahme nach Programm 2.20 oft, doch ist sie unhandlich, wenn eine weit von der Wurzelklasse entfernte Klasse auf Kommandozeilen
zugreifen soll. Dafür schreibt der Eiffel Library Standard Vintage 95 für alle EiffelImplementationen die Klasse ARGUMENTS mit folgenden Merkmalen vor:
command_name : STRING
argument_count : INTEGER
argument (i : INTEGER) : STRING
Der Kommandoname stimmt mit dem 0-ten Argument überein:
command_name = argument (0)
Problem
Bild 2.2
Benutzen und beerben
Eine von der Wurzelklasse verschiedene Klasse REMOTE_ARGUMENTS_WRITER soll
mit den Merkmalen von ARGUMENTS Kommandoargumente ausgeben.
CLIENT_OF_ARGUMENTS
use
ARGUMENTS
inherit
DESCENDANT_OF_ARGUMENTS
Auf Merkmale einer Klasse lässt sich zugreifen durch Benutzen oder Beerben der
Klasse. Welche Art der Strukturierung,
die Kunde-Lieferant-Beziehung oder
die Vorgänger-Nachfolger-Beziehung,
ist hier angemessen? Allgemeiner: In welchen Situationen ist welche Alternative zu
wählen? Beerben ist bequemer als Benutzen hinzuschreiben, hier etwa so:
Programm 2.21
Eiffel:
Argumentübernahme
mit Mix-In
class REMOTE_ARGUMENTS_WRITER
inherit
ARGUMENTS
feature
print_arguments
do
io.put_string (command_name)
end -- print_arguments
end -- class REMOTE_ARGUMENTS_WRITER
Mix-In-Vererbung
27.9.12
Der inherit-Abschnitt drückt aus, dass REMOTE_ARGUMENTS_WRITER von ARGUMENTS erbt. Das von ARGUMENTS geerbte Merkmal command_name ist in
REMOTE_ARGUMENTS_WRITER ohne Qualifizierung mit einem Objektnamen zugreif-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 18
2 Einmal quer durch die Sprachen
bar. Aber hier ist Vererbung nicht im Sinne der reinen Lehre eingesetzt (denn ein Argumentenschreiber ist kein Spezialfall von Argumenten), sondern nur als pragmatisches
Mix-In, damit REMOTE_ARGUMENTS_WRITER die Merkmale von ARGUMENTS direkt
benutzen kann. Folglich ist die Nachfolgerklasse REMOTE_ARGUMENTS_WRITER
unnötig eng an die Vorgängerklasse ARGUMENTS gekoppelt, ihre Schnittstelle und ihr
Namensraum sind unnötig aufgebläht. Besser ist es, die Leitlinie LS.17 des Dokuments
LS Leitlinien zum Softwareentwurf zu beachten:
Leitlinie 2.5
Bevorzuge Benutzung
vor Mix-Ins
Setze Vererbung nur für nicht einschränkende Spezialisierung ein, die die Ist-einRelation und das liskovsche Ersetzungsprinzip erfüllt! Vermeide Mix-Ins; benutze
ein Objekt der gewünschten Klasse, anstatt sie zu beerben!
Die angemessene Alternative ist hier also Benutzung: Die Kundenklasse
REMOTE_ARGUMENTS_WRITER kennt ein Objekt der Lieferantenklasse ARGUMENTS
und benutzt dieses Objekt. Dazu ist eine Größe des Typs ARGUMENTS statisch zu vereinbaren und das Objekt, auf das sich die Größe beziehen soll, dynamisch zu erzeugen.
Das erfordert etwas mehr Schreibaufwand auch beim Qualifizieren der Zugriffe, den
für bessere Softwarequalität – lose Kopplung, saubere Schnittstelle, kleiner Namensraum der Kundenklasse – zu investieren lohnt.
Bild 2.3
Kunden-LieferantenKette
CLIENT_OF_REMOTE_ARGUMENTS_WRITER
REMOTE_ARGUMENTS_WRITER
ARGUMENTS
CLIENT_OF_REMOTE_ARGUMENTS_WRITER, eine REMOTE_ARGUMENTS_WRITER
benutzende Wurzelklasse, komplettiert die Lösung zu einem Beispiel mit drei Klassen
und zwei Benutzungsbeziehungen.
Programm 2.22
Eiffel:
Argumentübernahme
irgendwo ohne Mix-In
note
description: "Output of command arguments"
class REMOTE_ARGUMENTS_WRITER
feature
print_arguments
-- Print arguments passed from the command-line.
local
arguments : ARGUMENTS
i
: INTEGER
do
create arguments
io.put_string ("command name: " + arguments.command_name)
io.put_new_line
from
i := 0
until i > arguments.argument_count loop
io.put_string ("argument " + i.out + ": " + arguments.argument (i))
io.put_new_line
i := i + 1
end
end -- print_arguments
end -- class REMOTE_ARGUMENTS_WRITER
In Programm 2.22 hat print_arguments einen lokalen Vereinbarungsteil, in dem durch
Größenvereinbarung
arguments : ARGUMENTS
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.2 Argumentübernahme von der Kommandozeile
2 – 19
die lokale Größe arguments des Typs ARGUMENTS vereinbart ist. Damit ist die Benutzungsbeziehung zwischen REMOTE_ARGUMENTS_WRITER und ARGUMENTS etabliert.
arguments ist eine Referenz, die sich nach ihrer Vereinbarung auf nichts bezieht. Das
Objekt, auf das sich arguments beziehen soll, wird durch die Erzeugungsanweisung
Objekterzeugung
create arguments
im Anweisungsteil dynamisch erzeugt. Nachdem sich arguments auf das neue Objekt
bezieht, kann es mit dem Punkt dereferenziert und z.B. die Abfrage command_name
selektiert werden:
Merkmalszugriff
arguments.command_name
ARGUMENTS ist ein Beispiel für eine Klasse, von der kaum ein Programm mehrere
Objekte benötigt. Doch Eiffel unterstützt Singletons und Module nicht durch ein spezielles Sprachkonstrukt. REMOTE_ARGUMENTS_WRITER kommt ohne Erzeugungsprozedur aus, da es nichts zu initialisieren gibt.
Benutzung
Programm 2.23
Eiffel: Wurzelklasse zur
Argumentausgabe
Dagegen muss CLIENT_OF_REMOTE_ARGUMENTS_WRITER als Wurzelklasse eine
Erzeugungsprozedur besitzen (Programm 2.23). make vereinbart eine lokale Größe writer des Typs REMOTE_ARGUMENTS_WRITER und etabliert so die Benutzungsbeziehung
zwischen beiden Klassen. Von Programm 2.22 ist bekannt, dass writer eine Referenz
ist, die sich nach ihrer Vereinbarung auf nichts bezieht. Erst nach der Erzeugungsanweisung
note
description: "Demonstrate usage of class REMOTE_ARGUMENTS_WRITER"
class CLIENT_OF_REMOTE_ARGUMENTS_WRITER
create
make
feature
make
-- Let some object print arguments passed from the command-line.
local
writer : REMOTE_ARGUMENTS_WRITER
do
create writer
writer.print_arguments
end -- make
end -- class CLIENT_OF_REMOTE_ARGUMENTS_WRITER
Objekterzeugung
create writer
bezieht sich writer auf das neue dynamisch erzeugte Objekt, sodass es mit dem Punkt
dereferenziert und das Kommando print_arguments selektiert werden kann:
Merkmalszugriff
writer.print_arguments
2.2.3
C++
2.2.3.1
Argumentübernahme
Älter als die obigen Eiffel-Mechanismen ist die Argumentübernahme von der Kommandozeile in C/C++. Programm 2.24 übernimmt beliebig viele Argumente von der
Kommandozeile und gibt sie aus.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 20
Programm 2.24
C++:
Argumentübernahme
Reihung
2 Einmal quer durch die Sprachen
#include <iostream>
int main (int argc, char * argv[]) {
for (int i = 0; i < argc; ++i) {
std::cout << "argument " << i << ": " << argv[i] << std::endl;
}
return 0;
}
Kommandoargumente werden von der Kommandozeile an main übergeben. Der ganzzahlige Parameter argc von main enthält die Anzahl der Kommandoargumente, die als
nullterminierte Zeichenketten in argv[0] bis argv[argc-1] übergeben werden. argc ist
erforderlich, weil die Länge von argv nicht dynamisch festzustellen ist. argv ist eine
offene Reihung ([]) von Zeigern (*) auf Zeichen (char); pascalisch würde man statt
char * argv[]
schreiben:
argumentVector : ARRAY OF POINTER TO CHAR,
und es gälte:
LEN (argumentVector) = argumentCount.
Die Erbschaft von C, Zeichenketten mit Zeigern auf Zeichen zu bearbeiten, ist fehlerträchtig. Eine weitere Fehlerquelle ist, dass prinzipiell nicht zur Laufzeit prüfbar ist, ob
ein Reihungsindex gültig ist. Dies sind zwei der Gründe, weshalb C++ nicht speicherund daher nicht typ- und nicht modulsicher ist. Bei der Argumentübernahme lässt sich
die folgende Leitlinie leider nicht anwenden.
Leitlinie 2.6
C++: Vermeide
unsichere CReihungen
Zählschleife
Verwende anstelle der unsicheren C-Reihungen und C-Zeichenketten die sicheren
Klassen vector und string der Standard Template Library!
main gibt die erhaltenen Argumente mit der als Zählschleife benutzten for-Schleife aus:
for (int i = 0; i < argc; ++i) {
std::cout << "argument " << i << ": " << argv[i] << std::endl;
}
Die for-Schleife in C++ ist eine kopfgesteuerte Bedingungsschleife mit Vereinbarungsund Initialisierungsteil, Fortsetzungsbedingung und Schleifenrumpfendeausdrucksfolge der Form
Schleifenmuster
for (initiale Ausdrücke; Fortsetzungsbedingung; Rumpfendeausdrücke) {
Schleifenrumpfanfangsanweisungen
}
Ihre Semantik ist mit einer maschinennahen Sprache der Abstraktionsebene 1.3(2) S. 114 erklärt durch
for-Schleife
maschinennah
Loop:
initiale Ausdrücke
if not Fortsetzungsbedingung then goto Next
Schleifenrumpfanfangsanweisungen
Rumpfendeausdrücke
goto Loop
Next:
Diese Art, die Semantik programmiersprachlicher Konstrukte durch Abbilden auf eine
einfachere, weniger abstrakte Sprache zu beschreiben, heißt operationale Semantik.
Fabel und Fakt
Exkurs. Ist die for-Schleife der C*-Sprachen mächtiger als andere Schleifenarten, wie in der
Literatur oft behauptet, aber nie bewiesen wird? Die obige operationale Semantik belegt, dass
die for-Schleife nicht mehr als eine kopfgesteuerte Bedingungsschleife leistet. Die for-Schleife
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.2 Argumentübernahme von der Kommandozeile
2 – 21
erledigt nichts implizit; alle Aktionen sind vom Programmierer zu spezifizieren, nur ihre textuelle Reihenfolge weicht von ihrer Ausführungsreihenfolge ab.
Oben wird durch
int i = 0
eine nur in der Schleife gültige Ganzzahlvariable i vereinbart und mit 0 initialisiert.
i < argc
ist eine Fortsetzungsbedingung, die am Kopf der Schleife ausgewertet wird. Ist der
Wert 0, was false entspricht, so wird die Schleife beendet, sonst fortgesetzt.
++
++i
ist ein Ausdruck, der am Ende des Schleifenrumpfs ausgewertet und dessen Wert weggeworfen wird. Sinnvoll ist das nur, wenn der Ausdruck einen Nebeneffekt bewirkt.
Hier wird der Wert von i durch den Präinkrementoperator ++ als Nebeneffekt um 1
erhöht, der neue Wert wird als Resultat zurückgegeben und weggeworfen.
Ein Test zeigt, dass wie bei Eiffel das 0-te Argument der Kommandoname ist.
2.2.3.2
Konversion
Sind die Zeichenketten in Zahlen zu konvertieren, so ist die von C übernommene Standardschnittstellendatei cstdlib.h durch die Vorübersetzerdirektive
#include <cstdlib>
zu inkludieren und die Zeile
std::cout << "argument " << i << ": " << argv[i] << std::endl;
durch
std::cout << "argument " << i << ": " << argv[i] <<
", as int: " << std::atoi(argv[i]) << ", as double: " << std::atof(argv[i]) << std::endl;
zu ersetzen. Die Lösung ist funktional, da zum Konvertieren die Funktionen
int atoi (const char * string);
double atof (const char * string);
von cstdlib.h dienen. Es gibt keine Funktion, die prüft, ob eine Zeichenkette eine Ganzoder Gleitpunktzahl enthält. atoi und atof liefern bei Misserfolg 0 bzw. 0.0 zurück. Es ist
dann nicht feststellbar, ob 0 eingegeben wurde oder einen Misserfolg anzeigt.
Der Entwurf dieser Funktionen aus den 1970er Jahren widerspricht heutigen Softwarequalitätsanforderungen. Die Funktionsnamen aus Unix-Kindheitstagen verstümmeln
„argument to integer“ bzw. „argument to float“ auf die damaligen maximal fünf Zeichen (im zweiten Fall obwohl das Ergebnis nicht float, sondern double ist). Eine Größe
wie eine Variable, einen Parameter oder ein Funktionsergebnis für mehrere Zwecke zu
verwenden, wenn sich die Wertebereiche für die Zwecke überlappen, ist Unfug.
Leitlinie 2.7
Ordne jedem Zweck
eigene Größen zu
Verwende jede Größe für nur einen Zweck! Der Wert jeder Größe muss stets eine
eindeutige Bedeutung haben. Dient eine Größe ausnahmsweise zwei Zwecken, so
garantiere, dass die Wertebereiche für beide Zwecke disjunkt sind!
2.2.4
Java
2.2.4.1
Argumentübernahme
Java erbt und verbessert den C++-Mechanismus der Argumentübernahme, verändert
ihn aber insofern, als der Kommandoname nicht als Argument erscheint. Programm
2.25 übernimmt beliebig viele Argumente von der Kommandozeile und gibt sie aus.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 22
Programm 2.25
Java:
Argumentübernahme 1
2 Einmal quer durch die Sprachen
public class ArgumentsWriter1 {
public static void main (String[] args) {
for (int i = 0; i < args.length; ++i) {
System.out.println("argument " + i + ": " + args[i]);
}
}
}
Reihung
Wie bei C++ übernimmt main die Argumente von der Aufrufstelle. Der formale Parameter args, der nicht fehlen darf, ist eine offene Reihung von Objekten der Standardbibliotheksklasse String. Unsichere Zeiger entfallen ebenso wie ein Anzahlparameter,
da die Länge von args dynamisch abfragbar ist, nämlich durch args.length. Java
prüft die Gültigkeit von Reihungsindizes zur Laufzeit.
Die for-Schleifen in Java und C# stimmen im Wesentlichen mit der for-Schleife von
C++ überein. C#s foreach-Schleife adaptierend führte Java J2SE 5.0 eine Für-alleSchleife (for-each-loop) zum Iterieren durch Behälter ein. In Programm 2.26 iteriert die
Für-alle-Schleife, die auch das Schlüsselwort for verwendet, durch die Reihung args,
allerdings ohne die Argumente zu zählen.
Programm 2.26
Java:
Argumentübernahme 2
public class ArgumentsWriter2 {
public static void main (String[] args) {
for (String s : args) {
System.out.println(s);
}
}
}
2.2.4.2
Konversion
Sind die Zeichenketten in Zahlen zu konvertieren, so ist die Zeile
System.out.println("argument " + i + ": " + args[i]);
zu ersetzen durch
try {
System.out.print("argument " + i + ": " + args[i]);
System.out.print(", as float: " + Float.parseFloat(args[i]));
System.out.print(", as int: " + Integer.parseInt(args[i]));
} catch (Exception e) {
// A NumberFormatException is caught.
System.out.print(", no float or no int - " + e.toString());
} finally {
System.out.println();
}
Hüllenklasse
Die Lösung kombiniert objektorientiert-modulare und funktionale Elemente mit Ausnahmebehandlung (exception handling). Java nennt die einfachen sprachdefinierten
Datentypen primitive Typen. Zu jedem primitiven Typ enthält das Standardbibliothekspaket java.lang eine Hüllenklasse (wrapper class), um Werte primitiver Typen in
Objekte packen zu können. Die Hüllenklassen enthalten aber auch viele Elemente, darunter Klassenfunktionen zur Konversion. Von der Hüllenklasse Integer wird
public static int parseInt (String s)
throws NumberFormatException
und von der Hüllenklasse Float wird
public static float parseFloat (String s)
throws NumberFormatException
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.2 Argumentübernahme von der Kommandozeile
2 – 23
benutzt. Es gibt keine Funktionen zum Prüfen, ob eine Zeichenkette eine Ganz- oder
Gleitpunktzahl darstellt. Bei Misserfolg lösen parseInt und parseFloat Zahlenformatausnahmen aus. Für das Verständnis der Ausnahmebehandlung soll hier dies genügen: Wird im try-Block
Ausnahmeauslösung
try { ... }
eine Ausnahme (exception) ausgelöst (geworfen), so verlässt der Ablauf die Auslösestelle direkt, um im catch-Block
Ausnahmebehandlung
catch (Exception e) { ... }
die Ausnahme zu behandeln (aufzufangen). Der optionale finally-Block
finally { ... }
wird stets ausgeführt: falls keine Ausnahme auftritt nach dem try-Block, sonst nach
dem catch-Block (auch wenn eine weitere Ausnahme ausgelöst wird und vor einer
return-Anweisung). Danach endet die Ausführung des try-catch-finally-Konstrukts. Die Reihenfolge „erst eine Gleitpunktzahl, dann eine Ganzzahl parsen“ verhindert, dass parseInt eine Ausnahme auslöst, bevor eine Gleitpunktzahl erkannt ist.
Leitlinie 2.8
Vermeide leere catchBlöcke
Ausnahmebehandlung als Programmierkonzept ermöglicht, angemessen auf im Programmablauf auftretende Fehler zu reagieren. Ausnahmen zeigen Fehler an. Für
Ausnahmen eine Behandlung vorzusehen, diese aber leer zu lassen, verschlimmert
jeden Fehler und ist daher Unfug. Behandle in jedem catch-Block die möglichen
Ausnahmen mit passenden Anweisungen!
Das Lösungsschema sei funktionales Schema mit Ausnahme genannt:
Funktionales Schema
mit Ausnahme
/* Try to produce target from any source: */
try {
target = service.target(source);
// May throw an exception.
process target
} catch (Exception e) {
handle bad case
} finally {
common action for good and bad case
}
2.2.5
C#
2.2.5.1
Argumentübernahme
Die erste Lösungsvariante in C#, Programm 2.27, unterscheidet sich kaum von der
Java-Variante Programm 2.25. Auch C# lässt den Kommandonamen nicht als Argument erscheinen.
Programm 2.27
C#:
Argumentübernahme 1
public class ArgumentsWriter1 {
public static void Main (string[] args) {
for (int i = 0; i < args.Length; ++i) {
System.Console.WriteLine("argument {0}: {1}", i, args[i]);
}
}
}
Variable
Parameteranzahl
27.9.12
WriteLine hat ähnlich wie printf in C eine variable Anzahl von Parametern, deren
Werte in Zeichenketten gewandelt in den ersten Parameter eingesetzt werden; in der
Ausgabezeile für {0} der Wert von i, für {1} der Wert von args[i].
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 24
2 Einmal quer durch die Sprachen
Programm 2.28
C#:
Argumentübernahme 2
public class ArgumentsWriter2 {
public static void Main (string[] args) {
foreach (string s in args) {
System.Console.WriteLine(s);
}
}
}
Die zweite C#-Variante, Programm 2.28, benutzt die Javas Für-alle-Schleife entsprechende foreach-Schleife, um durch die Reihung args zu iterieren, wie jene ohne die
Argumente zu zählen. Syntaktisch unterscheidet sich diese Schleife von jener durch das
andere Schlüsselwort und in statt „:“.
2.2.5.2
Konversion
Für die Konversion von Zeichenketten in Zahlen bietet C# zwei Varianten, die sich in
Details unterscheiden, und seit Version 2.0 eine dritte.1 Die ersten beiden Varianten
lösen das Problem möglicher Konversionsmisserfolge wie Java durch Ausnahmebehandlung. Die erste Variante folgt Java auch, indem sie jede Konversionsfunktion
der Klasse ihres Zielwerts zuordnet. Bei dieser Variante ist in Programm 2.27 die Zeile
System.Console.WriteLine("argument {0}: {1}", i, args[i]);
zu ersetzen durch
try {
System.Console.Write("argument {0}: {1}", i, args[i]);
System.Console.Write
(", as double: {0}", System.Double.Parse(args[i]));
System.Console.Write
(", as int: {0}", System.Int32.Parse(args[i]));
} catch (System.Exception e) {
// An Exception is caught.
System.Console.Write(", no double or no int - " + e.Message);
} finally {
System.Console.WriteLine();
}
Double, Int32 und Exception sind Standardbibliotheksklassen im Namensraum
System. Die zweite Variante sammelt die Konversionsfunktionen in der Standardbibliotheksklasse System.Convert. Damit ist in Programm 2.27 der Block
try {
System.Console.Write("argument {0}: {1}", i, args[i]);
System.Console.Write
(", as double: {0}", System.Convert.ToDouble(args[i]));
System.Console.Write
(", as int: {0}", System.Convert.ToInt32(args[i]));
} catch (System.Exception e) {
// An Exception is caught.
System.Console.Write(", no double or no int - " + e.Message");
} finally {
System.Console.WriteLine();
}
einzusetzen. Zum Konvertieren bietet System.Convert die Klassenfunktionen
public static int ToInt32 (string value);
public static double ToDouble (string value);
1
http://blogs.msdn.com/csharpfaq/archive/2004/05/30/144652.aspx (Zugriff 2007-03-25).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.2 Argumentübernahme von der Kommandozeile
2 – 25
Da System.Convert nur Klassenelemente hat, fungiert es als Modul; die Lösung ist
also modular. Bei Misserfolg lösen ToInt32 und ToDouble Zahlenformat- oder Überlaufausnahmen aus, was an ihren Vereinbarungen nicht erkennbar ist.
Nachteilig an den beiden ausnahmeorientierten Varianten (wie an der Java-Variante)
zur Konversion ist ihre Ineffizienz.1 Deshalb bietet C# 2.0 eine dritte Variante, die an
die Component-Pascal-Lösung erinnert, aber statt Prozeduren mit Ein-/AusgabeSchema stilistisch fragwürdige nebeneffektbehaftete Klassenfunktionen der Klassen
System.Int32 und System.Double mit einem Ausgabeparameter für die konvertierte
Zahl und dem Funktionsergebnis für die Erfolgs-/Misserfolgsanzeige verwendet:
public static bool TryParse (string s, out int result);
public static bool TryParse (string s, out double result);
Auswahlanweisung
Nach den möglichen Funktionsergebnissen zu differenzieren erfordert eine Auswahlanweisung. Die ein- und zweiseitige Auswahlanweisung (if, if-else) der C*-Sprachen
funktioniert im Wesentlichen wie in Component Pascal. Unterschiede sind, dass in
jedem Zweig nur eine Anweisung stehen darf und die Anweisung nicht durch ein
Schlüsselwort abgeschlossen ist. Dadurch mögliche Mehrdeutigkeiten werden andernorts diskutiert. Vermeiden kann man sie, indem man stets mit { } geklammerte Blockanweisungen einsetzt; innerhalb der Klammern stehen Anweisungsfolgen. Damit lautet
der Block, der in Programm 2.27 einzusetzen ist:
System.Console.Write("argument {0}: {1}", i, args[i]);
int intArg;
double doubleArg;
if (System.Int32.TryParse(args[i], out intArg)) {
System.Console.WriteLine(", as int: {0}", intArg);
} else if (System.Double.TryParse(args[i], out doubleArg)) {
System.Console.WriteLine(", as double: {0}", doubleArg);
} else {
System.Console.WriteLine(", no int, no double");
}
C#s Lösung folgt also dem Ein-/Ausgabe-Schema mit Ergebnisrückgabe:
Ein-/Ausgabe-Schema
mit Ergebnisrückgabe
// Try to produce target from any source and
// check if target is usable:
if (service.compute(source, out target)) {
process target
} else {
handle bad case
}
Während Hellenen diese Lösung wegen des Nebeneffekts verschmähen, begeistern
sich Barbaren dafür, weil sie kein Zwischenspeichern des Ergebnisses und daher keine
Vereinbarung einer Ergebnisvariablen erfordert.
Leitlinie 2.9
Verwende
Ausgabeparameter nur
bei Prozeduren
2.2.6
Vermeide Ausgabeparameter bei Funktionen! Funktionen mit Ausgabeparametern
bewirken Nebeneffekte, die nach Leitlinie 2.1 zu vermeiden sind. Funktionen sollen
ein Ergebnis an die Aufrufstelle liefern und sonst nichts.
Fazit
Programmaufrufe mit Argumentübergabe erledigt der Kommandospracheninterpretierer
der Sprachumgebung (Component Pascal/BlackBox),
des Betriebssystems (Eiffel, C*).
1
27.9.12
http://www.developerfusion.co.uk/show/4650/ (Zugriff 2007-03-26).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 26
2 Einmal quer durch die Sprachen
Die Anzahl der Argumente ist
beschränkt (Component Pascal),
unbeschränkt (Eiffel, C*).
Die Typen der Argumente sind
Zeichenketten (Component Pascal, Eiffel, C*),
Ganzzahlen (Component Pascal).
Die Argumente empfängt
die Prozedur, bei der der Programmablauf startet (Component Pascal, Eiffel, C*),
eine beliebige Prozedur (Eiffel).
Die Konversion von Zeichenketten in Zahlen übernehmen
ein Modul zur Zeichenkettenbearbeitung (Component Pascal),
die Zeichenkettenklasse (Eiffel),
Funktionen (C++),
die Hüllenklassen der Zieltypen (Java, C#),
eine allgemeine Konversionsklasse (C#).
Die bei der Konversion möglichen Misserfolge werden durch
Anwenden des Abfrage-Abfrage-Schemas (Eiffel)
von vornherein verhindert oder im Nachhinein angezeigt durch
einen Ausgabeparameter (Component Pascal) oder Rückgabewert (C#),
einen speziellen Funktionsrückgabewert, der auch ein korrektes Konversionsergebnis sein könnte (C++), oder
Auslösen von Ausnahmen (Java, C#).
Aufgabe 2.3
Kommandoargumente
Testen Sie die Argumentübernahme von der Kommandozeile mit den Programmen von
2.2! Ergänzen Sie die Programme um Konversionen von Zeichenketten in Zahlen!
Sprachen: Alle.
2.3
Uhrzeit
Problem
Am Beispiel der Uhrzeit sieht man
die Definition einer datenzentrierten Klasse mit
einer vertraglich spezifizierten Schnittstelle,
Vertrautheit mit der Methode der Spezifikation durch Vertrag vorausgesetzt. Die Klasse
erfüllt bekannte Konsistenzbedingungen und bietet Operationen zum Setzen und Weitersetzen einer Uhrzeit. Da die Standardinitialisierung hier konsistente Objekte liefert,
entfallen Initialisierungsproblem, Erzeugungsprozeduren und Konstruktoren. Dafür
betrachten wir verschiedene Objektsemantiken wie
Wertsemantik mit statisch vereinbarten Objekten (Wertobjekten), die im globalen
Datenbereich oder im Laufzeitkeller (runtime stack) liegen,
Zeiger- und Referenzsemantik mit dynamisch erzeugten Objekten (Zeiger- bzw.
Referenzobjekten), die auf der Halde (heap) liegen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.3 Uhrzeit
2.3.1
Konstante
Typ
Programm 2.29
Component Pascal:
Uhrzeit mit EmpfängerReferenzen
2 – 27
Component Pascal
Component Pascal schachtelt Klassen in Module, die zu Subsystemen gehören. Die
erste Form der Klassendefinition erlaubt sowohl Wert- als auch Zeigerobjekte. Für eine
Klasse wird konventionell ein Verbundtyp und ein zugehöriger Zeigertyp definiert.
Konstanten werden nicht in, sondern neben der Klasse im Modul vereinbart.
MODULE I3Clocks1;
IMPORT
BEC := BasisErrorConstants;
CONST
hoursPerDay*
= 24;
minutesPerHour* = 60;
TYPE
Clock* = POINTER TO ClockDesc;
ClockDesc* =
EXTENSIBLE RECORD
hour-,
minute- : INTEGER;
END;
PROCEDURE IsValidTime* (hour, minute : INTEGER) : BOOLEAN;
BEGIN
RETURN
(0 <= hour) & (hour < hoursPerDay) & (0 <= minute) & (minute < minutesPerHour);
END IsValidTime;
PROCEDURE (VAR clock : ClockDesc)
Set* (hour, minute : INTEGER), NEW, EXTENSIBLE;
BEGIN
ASSERT (IsValidTime (hour, minute), BEC.precondition);
clock.hour
:= hour;
clock.minute := minute;
ASSERT (IsValidTime (clock.hour, clock.minute), BEC.invariant);
END Set;
PROCEDURE (VAR clock : ClockDesc) Tick*, NEW, EXTENSIBLE;
BEGIN
ASSERT (IsValidTime (clock.hour, clock.minute), BEC.invariant);
clock.minute := (clock.minute + 1) MOD minutesPerHour;
IF clock.minute = 0 THEN
clock.hour := (clock.hour + 1) MOD hoursPerDay;
END;
ASSERT (IsValidTime (clock.hour, clock.minute), BEC.invariant);
END Tick;
END I3Clocks1.
Component Pascal erlaubt vollen Export von Datenelementen (Modulvariablen, Verbundfeldern), sodass Kunden lesend und schreibend darauf zugreifen können. Aber
gegen Datenabstraktion und die Leitlinie LS.28 des Dokuments LS Leitlinien zum Softwareentwurf zu verstoßen ist selten sinnvoll; ausgenommen sind z.B. Interaktoren zu
Dialogen.
Leitlinie 2.10
Component Pascal:
Verberge Daten hinter
Schnittstellen
Explizit
schreibgeschützter
Export
27.9.12
Vermeide Schreibzugriffe erlaubenden Export von Datenelementen, da dies dem
Konzept der Datenabstraktion widerspricht! Exportiere Datenelemente höchstens
schreibgeschützt, damit die exportierende Einheit die Gültigkeit ihrer Invarianten
garantieren kann!
Component Pascal differenziert den Export von Diensten nach Zugriffsarten. Datenelemente lassen sich so exportieren, dass Kunden nur lesend darauf zugreifen können.
Schreibgeschützter Export
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 28
2 Einmal quer durch die Sprachen
☺ erlaubt eine bequeme Form der Datenabstraktion ohne lästige Programmierung von
Typisierter Empfänger
Lesezugriffsfunktionen auf Datenelementen,
macht die Implementation von Abfragen als Datenelemente an der Schnittstelle
sichtbar. Ist z.B. die Abfrage der Stunde als schreibgeschütztes Feld implementiert,
so lautet ein Zugriff clock.hour, ist sie als parameterlose Funktion implementiert, so
lautet ein Aufruf clock.Hour (). Die unterschiedliche Syntax und die Konvention der
Klein-Großschreibung verletzen das Konzept der Bezugstransparenz, der Unabhängigkeit des Zugriffs von der Implementation.
Eine Spezialität von Component Pascal ist der frei wählbare Name für das Empfängerobjekt, der konsequenterweise zu typisieren ist – mit dem Verbundtyp oder dem Zeigertyp der Klasse. Daraus ergeben sich zwei Formen der Klassendefinition.
Erweiterbarkeit
Die Angabe von EXTENSIBLE beim Verbundtyp und von NEW und EXTENSIBLE bei
jeder typgebundenen Prozedur scheint verbos. Die Idee bei Component Pascal ist, dass
Programmierer von Komponenten abstrakte Klassen definieren und Anwendungsprogrammierer Erweiterungen dieser Klassen implementieren. Die Sprache ist so entworfen, dass die wenigen Komponentenprogrammierer mehr, die vielen Anwendungsprogrammierer weniger schreiben müssen. Konkret: Bei Basistypen sind die
Spezifikatoren ABSTRACT, LIMITED, NEW, EXTENSIBLE anzugeben, bei Erweiterungsklassen meist nichts.
Spezifikation
Als Spezifikationsmittel bietet Component Pascal nur Zusicherungen in Form der
Standardprozeduren ASSERT. Damit lassen sich Verträge eingeschränkt formulieren.
Komplexe Invarianten sind in Prozeduren zusammenzufassen, was hier unnötig ist. Da
die Prüfprozedur IsValidTime unabhängig von Objektdaten ist, ist sie an das Modul,
nicht die Klasse gebunden. Die offensichtlichen Nachbedingungen bei der Set-Operation sind weggelassen, um das Beispiel knapp zu halten.
Beispiel für Wertobjekt
VAR clock : I3Clocks1.ClockDesc;
...
clock.Set (12, 30);
clock.Tick;
(* RECORD, nicht initialisiert *)
Beispiel für
Zeigerobjekt
VAR clock : I3Clocks1.Clock;
...
NEW (clock);
clock^.Set (12, 30);
clock.Tick;
(* POINTER TO RECORD *)
(* Objekterzeugung, Standardinitialisierung *)
(* Explizite Dereferenzierung *)
(* Implizite Dereferenzierung *)
Die zweite Form der Klassendefinition erlaubt nur Zeigerobjekte.
Programm 2.30
Component Pascal:
Uhrzeit mit EmpfängerZeigern
MODULE I3Clocks2;
(* Stuff not shown same as in Programm 2.29. *)
TYPE
Clock* = POINTER TO EXTENSIBLE RECORD ... END;
PROCEDURE (clock : Clock) Set* (hour, minute : INTEGER), NEW, EXTENSIBLE; ...
PROCEDURE (clock : Clock) Tick*, NEW, EXTENSIBLE; ...
END I3Clocks2.
Beispiel für
Zeigerobjekt
VAR clock : I3Clocks2.Clock;
...
NEW (clock);
clock^.Set (12, 30);
clock.Tick;
(* POINTER TO RECORD *)
(* Objekterzeugung, Standardinitialisierung *)
(* Explizite Dereferenzierung *)
(* Implizite Dereferenzierung *)
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.3 Uhrzeit
2.3.2
2 – 29
Eiffel
Die in Eiffel übliche Form der Klassendefinition erlaubt Referenzobjekte. Die Struktur
von Component Pascal, das in Programm 2.29 die Klasse Clock in das Modul Clocks im
Subsystem I3 schachtelt, lässt sich mit Eiffels Sprachmitteln kaum nachbilden. Alle
Merkmale gehören zur Klasse. Die Konstanten Hours_per_day, Minutes_per_hour müssen für alle CLOCK-Objekte nur einmal Speicherplatz belegen.
Programm 2.31
Eiffel: Uhrzeit nicht
expandiert
class CLOCK
feature
Hours_per_day
: INTEGER = 24
Minutes_per_hour : INTEGER = 60
hour,
minute
: INTEGER
is_valid_time (an_hour, a_minute : INTEGER) : BOOLEAN
do
Result :=
0 <= an_hour and an_hour < Hours_per_day and
0 <= a_minute and a_minute < Minutes_per_hour
end -- is_valid_time
set (new_hour, new_minute : INTEGER)
require
arguments_valid:
is_valid_time (new_hour, new_minute)
do
hour
:= new_hour
minute := new_minute
ensure
new_hour_accepted:
hour = new_hour
new_minute_accepted: minute = new_minute
end -- set
tick
do
minute := (minute + 1) \\ Minutes_per_hour
if minute = 0 then
hour := (hour + 1) \\ Hours_per_day
end
ensure
next_minute_set: minute = (old minute + 1) \\ Minutes_per_hour
next_hour_set:
minute = 0 implies hour = (old hour + 1) \\ Hours_per_day
end -- tick
invariant
state_valid: is_valid_time (hour, minute)
end -- class CLOCK
Implizit
schreibgeschützter
Export
Die nicht konstanten Attribute hour und minute sind zwar als Teil eines featureAbschnitts ohne Kundenliste öffentlich, aber Eiffel exportiert Attribute prinzipiell
schreibgeschützt. Ein Kunde darf bei einer Größe clock der Klasse CLOCK zwar
clock.hour abfragen, kann aber nie Zuweisungen der Form
clock.hour := 4711
enthalten. Wie Smalltalk setzt Eiffel das Konzept der Datenabstraktion streng um,
damit jede Klasse die Gültigkeit ihrer Invarianten garantieren kann. Lesezugriffsfunktionen auf Attribute sind in Eiffel unnötig. Das Konzept der Bezugstransparenz und die
Leitlinie LS.28 in LS Leitlinien zum Softwareentwurf werden eingehalten: Am Aufruf
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 30
2 Einmal quer durch die Sprachen
clock.hour ist nicht erkennbar, ob die Abfrage hour als Attribut oder als parameterlose
Funktion implementiert ist.
Erweiterbarkeit
Ohne Angaben bei ihrer Definition ist die Klasse beerbbar, ihre Merkmale sind in
Nachfolgerklassen redefinierbar. Der Schreibaufwand fällt beim Redefinieren an.
Redefinition ist in Eiffel die einzige Möglichkeit, Namen mehrfach zu vereinbaren. Eiffel kennt weder Überladen noch Überdecken. Deshalb müssen sich Formalparameternamen von Attributnamen unterscheiden.
Spezifikation durch
Vertrag
Eiffel ist nicht nur eine Implementations-, sondern auch eine Spezifikationssprache. Es
unterstützt die Methode der Spezifikation durch Vertrag (Design by Contract, DbC) mit
Klasseninvarianten, Vor- und Nachbedingungen von Routinen, old-Ausdrücken in
Nachbedingungen, sowie einigen Spezialitäten. Jede Vertragsbedingung kann zur
Dokumentation eine Marke erhalten. Welche Bedingungen in welcher Klasse zur Laufzeit geprüft werden, wird bei der Kompilation festgelegt.
Result
Auf invariant folgen Invarianten der Klasse, also Bedingungen an ihre Abfragen,
die für jedes ihrer Objekte vor und nach jedem Routinenaufruf gelten müssen (außer
vor der Objektinitialisierung).
Auf require folgen Vorbedingungen der Routine, also Bedingungen an die Abfragen
und Routinenparameter, die vor dem Aufruf der Routine gelten müssen.
Auf ensure folgen Nachbedingungen der Routine, also Bedingungen an die Abfragen und Routinenparameter, die nach dem Aufruf der Routine gelten müssen, sofern
zuvor die Vorbedingungen erfüllt waren.
In Nachbedingungen bezeichnet old Expression den Wert von Expression vor der
Ausführung des Routinenaufrufs.
Result ist eine in jeder Funktion implizit vereinbarte lokale Größe des Resultattyps für
den Rückgabewert. Der Wert, den Result beim Erreichen des statischen Endes der Funktion hat, wird als Funktionsergebnis zurückgegeben. Result wird in Nachbedingungen
gebraucht. So spart Eiffel eine spezielle Rückkehranweisung.
is_valid_time wird u.a. in Invarianten aufgerufen. Vor und nach jeder Ausführung einer
Routine können die Invarianten geprüft werden, also auch bei is_valid_time. Das würde
zu endlosen rekursiven Aufrufen von is_valid_time führen, aber Eiffel verhindert das.
Bei Aufrufen von Abfragen innerhalb von Invariantenprüfungen werden weitere Invariantenprüfungen unterdrückt.
\\
Der Modulooperator \\ passt lexikalisch zum Ganzzahldivisionsoperator //.
In Eiffel ist Referenzsemantik üblich. Hier ist clock eine Referenz auf ein CLOCKObjekt, das mit einer Erzeugungsanweisung dynamisch auf der Halde angelegt wird:
Beispiel für
Referenzobjekt
clock : CLOCK
...
create clock
clock.set (12, 30)
clock.tick
-- Referenz, die sich auf nichts bezieht
-- Explizite Objekterzeugung, Standardinitialisierung
-- Implizite Dereferenzierung
Wertsemantik ist mit der Angabe von expanded bei der Größenvereinbarung möglich:1
Beispiel für Wertobjekt
clock : expanded CLOCK
...
clock.set (12, 30)
clock.tick
-- Objekt, per Standard initialisiert
Soll eine Klasse nur Wertobjekte ihres Typs ermöglichen, so kann man die Klasse als
expandiert spezifizieren.
1
Dieses Sprachmerkmal ist in [ER06], aber nicht im ECMA-Standard [ECMA367] enthalten
(Zugriff 2006-04-07).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.3 Uhrzeit
Programm 2.32
Eiffel: Uhrzeit
expandiert
2 – 31
expanded class CLOCK_EXPANDED
-- Stuff not shown same as in Programm 2.31.
end -- class CLOCK_EXPANDED
Beispiel für Wertobjekt
clock : CLOCK_EXPANDED
...
clock.set (12, 30)
clock.tick
-- Objekt, per Standard initialisiert
Die Markierung expanded vererbt sich nicht. Expandierte und nicht expandierte Klassen sind beliebig beerbbar, die Nachfolgerklassen können beliebig expandiert oder
nicht expandiert sein. Eiffel behandelt Expansion und Vererbung als orthogonale Konzepte. Möglich sind also folgende Definitionen.
Programm 2.33
Eiffel: Uhrzeit als
expandierter
Nachfolger von
Programm 2.31
Beispiel für Wertobjekt
Programm 2.34
Eiffel: Uhrzeit als nicht
expandierter
Nachfolger von
Programm 2.32
Beispiel für
Referenzobjekt
2.3.3
expanded class CLOCK_EXP
inherit
CLOCK
end -- class CLOCK_EXP
clock : CLOCK_EXP
...
clock.set (12, 30)
clock.tick
-- Objekt, per Standard initialisiert
class CLOCK_REFERENCED
inherit
CLOCK_EXPANDED
end -- class CLOCK_REFERENCED
clock : CLOCK_REFERENCED
...
create clock
clock.set (12, 30)
clock.tick
-- Referenz, die sich auf nichts bezieht
-- Explizite Objekterzeugung, Standardinitialisierung
-- Implizite Dereferenzierung
C++
Schon Programm 2.7 zeigt, wie der Quelltext einer C++-Klasse auf eine Schnittstellenund eine Implementationsdatei aufzuteilen ist. C++ kennt nur eine Form der Klassendefinition für Wert-, Referenz- und Zeigerobjekte. Component Pascals SubsystemModul-Klasse-Struktur kann C++ mit den geschachtelten Namensräumen I3 und
Clocks nachbilden. Die Klassenkonstanten hours_per_day, minutes_per_hour lassen sich
ausnahmsweise – da ganzzahligen Typs – innerhalb der Klassendefinition initialisieren.
C++, Java und C# erlauben bei Klassen öffentliche Datenelemente, sodass Kunden
lesend und schreibend darauf zugreifen können. Aber Klassen mit öffentlichen Daten
können die Gültigkeit ihrer Invarianten nicht garantieren. Die C*-Sprachen unterstützen die Leitlinie LS.28 in LS Leitlinien zum Softwareentwurf nicht gut.
Leitlinie 2.11
C*: Verberge Daten
hinter Schnittstellen
Geschütztes
Datenelement
27.9.12
Vermeide öffentliche Datenelemente, da dies dem Konzept der Datenabstraktion
widerspricht! Vereinbare Datenelemente stets geschützt!
Daher sind Datenelemente am besten so zu vereinbaren, dass nur die vereinbarende
Klasse und ihre Unterklassen darauf zugreifen können:
protected: int hour;
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 32
Programm 2.35
C++: Uhrzeit,
Schnittstelle
2 Einmal quer durch die Sprachen
// Header file: /Include/I3/Clocks/Clock.hpp:
#ifndef I3_CLOCKS_CLOCK_HPP_
#define I3_CLOCKS_CLOCK_HPP_
namespace I3 {
namespace Clocks {
class Clock {
protected:
int hour,
minute;
public:
static const int hours_per_day = 24;
static const int minutes_per_hour = 60;
int get_hour () const {return hour;}
int get_minute () const {return minute;}
static bool is_valid_time (int, int); // an_hour, a_minute
☺
virtual void set (int new_hour, int new_minute);
virtual void tick ();
}; // end of Clock
} // end of Clocks
} // end of I3
#endif // I3_CLOCKS_CLOCK_HPP_
Export: alles oder
nichts
Öffentliche
Lesezugriffsfunktion
Damit sind sie aber nicht nur schreib-, sondern auch lesegeschützt vor Kundenzugriffen. Datenabstraktion ist realisiert, aber zu stark! C* erlaubt bei Exporten entweder alle
Zugriffsarten oder gar keine. Der Schutz lässt sich in C* nicht wie in Component Pascal nach Zugriffsart differenzieren. Um Datenelemente vor externen Schreibzugriffen
zu schützen und externe Lesezugriffe darauf zu ermöglichen, definiert man öffentliche
Lesezugriffsfunktionen zu den geschützten Datenelementen:
public: int get_hour () const {return hour;}
Das const nach der Signatur zeigt an, dass die Funktion den Objektzustand belässt. In
C++ sind Namen von Datenelementen nicht überladbar. Daher erhält eine Lesezugriffsfunktion einen vom Namen ihres Datenelements abgeleiteten Namen. Üblich ist
das Präfix get_ trotz seines Schönheitsfehlers, denn ein Verb passt nicht zu einem Funktionsnamen, der das Funktionsresultat bezeichnen soll. Wegen dieser Konvention werden Lesezugriffsfunktionen oft als Getter-Funktionen oder kurz Getter bezeichnet.
Getter-Funktionen sind ein Idiom – ein einfaches sprachabhängiges Programmierschema – der C*-Sprachen. (Manche Autoren werten es zu einem Entwurfsmuster auf.)
In C++ implementiert man Zugriffsfunktionen gern als implizite Inline-Funktionen in
der Schnittstellendatei, wenn sie nur eine return-Anweisung enthalten. Ihr Vorteil ist,
dass ein Inline-Funktionsaufruf clock.get_hour() nicht mehr als ein direkter Lesezugriff
clock.hour auf das Datenelement kostet. Nachteile können beim Ableiten (Vererben)
auftreten, weil Inline-Funktionen nicht redefinierbar sind und natürlich stets statisch
gebunden werden. Im Vergleich erreicht Eiffel mit schreibgeschütztem Export von
Attributen dasselbe auf höherem Abstraktionsniveau mit weniger Schreibaufwand.
Bei Vereinbarungen von Funktionen sind ihre Signaturen relevant. In C++ kann man
die Namen von Formalparametern zwar weglassen, doch mindert dies die Lesbarkeit
der Schnittstelle.
Leitlinie 2.12
C++: Lesbar statt kurz
Gib bei Funktionsvereinbarungen für Formalparameter verständliche Namen an!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.3 Uhrzeit
Hellseherprinzip
2 – 33
Klassen sollen wiederverwendbar sein – durch Benutzen und Beerben. Das Hellseherprinzip (clairvoyance principle) besagt, dass der Entwickler einer Klasse nicht voraussehen kann und daher nicht voraussehen müssen sollte, wie die Klasse später wiederverwendet, zu welchen Klassen sie erweitert werden könnte. Deshalb soll eine Klasse
zwar ihre Schnittstelle exakt festlegen, aber auch später hinzukommenden Unterklassen erlauben, die spezifizierte Schnittstelle beliebig zu implementieren und zu erweitern. Insbesondere soll eine Klasse die Redefinierbarkeit ihrer Merkmale nur aus zwingenden Gründen einschränken.
In C++ muss eine Elementfunktion in der Basisklasse, in der sie zuerst erscheint, als
virtual vereinbart sein, damit sie in abgeleiteten Klassen redefinierbar (überschreibbar,
overridable) ist und in polymorphen Situationen dynamisch gebunden wird. Der Normalfall objektorientierten Programmierens fordert in C++ besondere Kennzeichnung!
Leitlinie 2.13
C++: Denke an
Redefinierbarkeit
Historisches
Programm 2.36
C++: Uhrzeit,
Implementation
Spezifiziere Elementfunktionen stets als virtual, um sie redefinierbar zu erhalten!
Ausgenommen davon sind Inline-Getter-Funktionen.
Exkurs. Die Unterscheidung zwischen einerseits so genannten virtuellen, redefinierbaren und
dynamisch gebundenen Prozeduren und andererseits nicht-virtuellen, überdeckbaren und statisch gebundenen Prozeduren stammt aus Simula 67, von dessen Klassenmodell C++ viel übernommen hat. Smalltalk hat redefinierbare Methoden, Polymorphie und dynamisches Binden zur
Standardsemantik erklärt. Component Pascal, Eiffel, Java und andere objektorientierte Sprachen sind dieser Richtung gefolgt, C# der anderen.
// Implementation file: /Source/I3/Clocks/Clock.cpp:
#include <cassert>
#include "Clock.hpp"
namespace I3 {
namespace Clocks {
bool Clock::is_valid_time (int an_hour, int a_minute) {
return
0 <= an_hour && an_hour < hours_per_day &&
0 <= a_minute && a_minute < minutes_per_hour;
}
void Clock::set (int new_hour, int new_minute) {
assert(is_valid_time(new_hour, new_minute)); // precondition
hour
= new_hour;
minute = new_minute;
assert(is_valid_time(hour, minute)); // invariant
}
void Clock::tick () {
assert(is_valid_time(hour, minute)); // invariant
minute = (minute + 1) % minutes_per_hour;
if (minute == 0) {
hour = (hour + 1) % hours_per_day;
}
assert(is_valid_time(hour, minute)); // invariant
}
} // end of Clocks
} // end of I3
Spezifikation mit
Zusicherung
Eine einfache, wenn auch primitiv funktionierende Möglichkeit für Zusicherungen in
C++ ist das von C geerbte assert-Vorübersetzermakro; es ist in cassert.h vereinbart.
= und ==
Leicht Verwechslungen verursacht, dass die C*-Sprachen für die Gleichheit statt des in
der Mathematik seit rund 350 Jahren weltweit üblichen Zeichens „=“ das „==“ ver-
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 34
2 Einmal quer durch die Sprachen
wenden, weil „=“ als Zuweisungsoperator dient. Der Missbrauch des Gleichheitszeichens als Zuweisungssymbol entstammt dem Fortran der 1950er Jahre; schon Algol 60
startete mit dem Zuweisungssymbol „:=“ eine weniger fehlerträchtige Alternative.
„=“ und „==“ zu verwechseln wäre allein nicht schlimm. Hinzu kommt die Ausdrucksorientierung der C*-Sprachen, deretwegen die Zuweisung ein Ausdruck ist. Dadurch
sind Vergleiche und Zuweisungen nicht syntaktisch unterschieden und können an denselben Stellen auftreten. Bei C++ kommt die schwache Typisierung hinzu, die nicht
zwischen booleschen und arithmetischen Ausdrücken differenziert. Wenigstens diesen
Mangel beseitigen Java und C#.
%
„%“ ist in den C*-Sprachen der Modulooperator.
Aufgabe 2.4
C++: Falsch getickt
Welcher Fehler ist dem Programmierer bei Clock::tick in der Anweisung
if (minute = 0) {
hour = (hour + 1) % hours_per_day;
}
unterlaufen und welche Folgen hat er? Ist die Anweisung übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Wie tickt diese Uhr?
C++ kennt nur eine Form der Klassendefinition für alle möglichen Speicher- und
Zugriffsarten der Objekte. Der Programmierer muss also die spätere Art der Benutzung
der Klasse in Form von Objekten nicht voraussehen.
Beispiel für Wertobjekt
Clock clock = Clock();
Clock clock;
...
clock.set(12, 30);
clock.tick;
// Standardinitialisierung mit Standardkonstruktor
// ebenso in abkürzender Notation
Beispiel für
Referenzobjekt
Clock & clock = Clock();
...
clock.set(12, 30);
clock.tick();
// Explizite Erzeugung und Initialisierung
Clock * clock = new Clock();
...
(*clock).set(12, 30);
clock->tick();
// Explizite Erzeugung und Initialisierung
Beispiel für
Zeigerobjekt
2.3.4
// Implizite Dereferenzierung
// Explizite Dereferenzierung
// Explizite Dereferenzierung
Java
Java kennt bei Klassen nur Referenzsemantik, d.h. es erlaubt nur Referenz-, keine Wertobjekte. Component Pascals Subsystem-Modul-Klasse-Struktur lässt sich in Java mit
den geschachtelten Paketen I3 und Clocks nachbilden.
Namen
Java erlaubt das Überladen von Datenelement- und Funktionsnamen. Deshalb heißen
die Lesezugriffsfunktionen wie ihre Datenelemente. Namen von Datenelementen und
Formalparametern dürfen wie in Component Pascal und C++ gleich sein. Ein Formalparametername überdeckt einen gleichen Datenelementnamen, was die Verständlichkeit mindern kann. Dennoch wird die Möglichkeit bei Setter-Methoden gern genutzt,
wobei die überdeckten Namen der Datenelemente mit this, dem Standardnamen der
C*-Sprachen für das aktuelle Objekt, zu qualifizieren sind.
Spezifikation mit
Zusicherung
Seit Release 1.4 bietet auch Java Zusicherungen; sie werden mit dem Schlüsselwort
assert eingeleitet, dem booleschen Ausdruck kann eine Zeichenkette folgen. Beim
Aufruf der JVM ist anzugeben, ob die Zusicherungen zur Laufzeit geprüft werden.1
1
http://java.sun.com/developer/technicalArticles/JavaLP/assertions/ (Zugriff 2008-11-24),
http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html (Zugriff 2008-11-24).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.3 Uhrzeit
Programm 2.37
Java: Uhrzeit
2 – 35
package I3.Clocks;
public class Clock {
public static final int HOURS_PER_DAY = 24;
public static final int MINUTES_PER_HOUR = 60;
private int hour,
minute;
public int hour () {return hour;}
public int minute () {return minute;}
public static boolean isValidTime (int hour, int minute) {
return
0 <= hour && hour < HOURS_PER_DAY &&
0 <= minute && minute < MINUTES_PER_HOUR;
}
public void set (int hour, int minute) {
assert isValidTime(hour, minute) : "precondition";
this.hour
= hour;
this.minute = minute;
assert isValidTime(this.hour, this.minute) : "invariant";
}
public void tick () {
assert isValidTime(hour, minute) : "invariant";
minute = (minute + 1) % MINUTES_PER_HOUR;
if (minute == 0) {
hour = (hour + 1) % HOURS_PER_DAY;
}
assert isValidTime(hour, minute) : "invariant";
}
} // end of Clock
Aufgabe 2.5
Java: Falsch getickt
Welcher Fehler ist dem Programmierer bei Clock.tick in der Anweisung
if (minute = 0) {
hour = (hour + 1) % HOURS_PER_DAY;
}
unterlaufen und welche Folgen hat er? Ist die Anweisung übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Wie tickt diese Uhr?
Beispiel für
Referenzobjekt
2.3.5
Clock clock = new Clock(); // Explizite Erzeugung und Initialisierung
...
clock.set(12, 30);
// Implizite Dereferenzierung
clock.tick();
C#
C#s Typsystem unterscheidet zwischen Wert- und Referenztypen. Klassen gehören zu
Referenztypen; sie erlauben daher nur Referenzobjekte. Component Pascals Subsystem-Modul-Klasse-Struktur lässt sich in C# mit den geschachtelten Namensräumen I3
und Clocks nachbilden, wobei sich die Schachtelung durch den Punkt ausdrücken
lässt.
Property
Für Setter- und Getter-Funktionen zu Datenelementen bietet C# eine Properties
genannte syntaktische Variante (von der Programm 2.38 nur den get-Teil benötigt).
Kunden können Properties wie Datenelemente benutzen:
h = clock.Hour;
clock.Hour = h;
27.9.12
statt
statt
h = clock.GetHour();
clock.SetHour(h);
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 36
Programm 2.38
C#: Uhrzeit als
Referenztyp
2 Einmal quer durch die Sprachen
namespace I3.Clocks {
public class Clock {
public const int HoursPerDay = 24;
public const int MinutesPerHour = 60;
protected int hour,
minute;
public int Hour {
get {return hour;}
}
public int Minute {
get {return minute;}
}
public static bool IsValidTime (int hour, int minute) {
return
0 <= hour && hour < HoursPerDay &&
0 <= minute && minute < MinutesPerHour;
}
public virtual void Set (int hour, int minute) {
System.Diagnostics.Debug.Assert
(isValidTime(hour, minute), "precondition");
this.hour
= hour;
this.minute = minute;
System.Diagnostics.Debug.Assert
(isValidTime(this.hour, this.minute), "invariant");
}
public virtual void Tick () {
System.Diagnostics.Debug.Assert
(isValidTime(hour, minute), "invariant");
minute = (minute + 1) % MinutesPerHour;
if (minute == 0) {
hour = (hour + 1) % HoursPerDay;
}
System.Diagnostics.Debug.Assert
(isValidTime(hour, minute), "invariant");
}
} // end of Clock
} // end of I3.Clocks
Namen
Leitlinie 2.14
C#: Denke an
Redefinierbarkeit
Spezifikation mit
Zusicherung
Anders als Java erlaubt C# kein Überladen von Datenelement-, Funktions- und Propertynamen. Die Namen eines Datenelements und seiner Property unterscheiden sich gern
nur in der Klein-Großschreibung des Anfangsbuchstabens. Namen von Datenelementen und Formalparametern dürfen wie in Java übereinstimmen.
Spezifiziere Elementfunktionen stets als virtual, um sie redefinierbar zu erhalten!
Auch C# bietet Zusicherungen, aber nicht wie Java als Sprachkonstrukt, sondern als
Funktion Assert der Bibliotheksklasse Debug, die mit Visual Basic 5.0 in das .NET
Framework eingeführt und im Namensraum System.Diagnostics verborgen wurde.
Dass die Zusicherungen zur Laufzeit geprüft werden, ist wie bei Eiffel und Java bei der
Kompilation festzulegen. Möge der lange vollqualifizierte Name
System.Diagnostics.Debug.Assert
keinen Programmierer davor abschrecken, Zusicherungen einzusetzen! Eine mögliche
Alternative ist, durch die Direktive
using System.Diagnostics;
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.3 Uhrzeit
2 – 37
vor der Klassendefinition den Namen auf Debug.Assert zu verkürzen, doch wegen der
softwaretechnischen Nachteile der using-Direktive rate ich davon ab.
Beispiel für
Referenzobjekt
Wertsemantik
Programm 2.39
C#: Uhrzeit als Werttyp
Clock clock = new Clock(); // Explizite Erzeugung und Initialisierung
...
clock.Set(12, 30);
// Implizite Dereferenzierung
clock.Tick();
Die C#-Terminologie kennt zwar keine Wertobjekte, man kann aber mit C#s structKonstrukt Werttypen definieren, deren Variablen Datenwerte speichern (die nicht
Objekte heißen). In der Typdefinition von Programm 2.38 ist das Schlüsselwort class
durch struct zu ersetzen.
namespace I3.Clocks {
public struct Clock {
// Stuff not shown same as in Programm 2.38 but without ’virtual’.
} // end of Clock
} // end of I3.Clocks
Beispiel für
Wertexemplar
Clock clock = Clock();
...
clock.Set(12, 30);
clock.Tick();
// Explizite Initialisierung
struct-
und andere Werttypen kennen keine Vererbung, sie können weder Ober- noch
Untertypen anderer Typen sein. Der Programmierer muss schon bei der Definition
eines Typs entscheiden, ob für seine Variablen stets Wertsemantik ohne Erweiterbarkeit
(struct) oder stets Referenzsemantik mit Erweiterbarkeit (class) gelten soll. Dies
widerspricht dem Hellseherprinzip. C# behandelt Speichersemantik und Erweiterbarkeit als nicht orthogonale Konzepte.
2.3.6
Fazit
Eine Definition einer wiederverwendbaren Klasse ist
ganz in einer Datei enthalten (Component Pascal, Eiffel, Java, C#),
in eine Schnittstellen- und eine Implementationsdatei aufgeteilt (C++),
auf mehrere Dateien verteilbar (C++, C#).
Klassendefinitionen
stehen nebeneinander (Eiffel),
sind in andere Sprachkonstrukte geschachtelt:
stets zweistufig in Subsysteme und Module (Component Pascal/BlackBox),
optional in beliebig viele Namensräume (C++, C#) oder Pakete (Java).
Zu einer Klasse gehörende Konstanten werden
neben der Klasse definiert und an das Modul gebunden (Component Pascal),
in der Klasse definiert und
implementationsabhängig bei der Klasse gespeichert, aber an Objekte gebunden
(Eiffel),
optional mit static bei der Klasse gespeichert und dann an die Klasse und
Objekte gebunden (C++, Java),
stets bei der Klasse gespeichert und an die Klasse und Objekte gebunden (C#).
Wie können Klassen Datenelemente (Felder, Attribute) exportieren?
Nur schreibgeschützt (Eiffel),
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 38
2 Einmal quer durch die Sprachen
optional schreibgeschützt (Component Pascal),
uneingeschränkt, was mit dem Konzept der Datenabstraktion konfligiert (Component Pascal, C++, Java, C#).
Die im letzten Fall für Datenabstraktion erforderlichen Getter-Funktionen lassen sich
implementieren als
Inline-Funktionen (C++),
Properties (C#).
Das aktuelle Objekt hat einen
programmiererdefinierten Namen (Component Pascal),
Standardnamen (Current in Eiffel, this in C++, Java, C#).
Spezifikation durch Vertrag wird
umfassend durch spezielle Sprachkonstrukte unterstützt (Eiffel),
eingeschränkt durch Zusicherungen ermöglicht (Component Pascal, C++, Java,
C#).
Zusicherungen sind realisiert durch
eine spezielle Anweisung (Java),
Standardprozeduren (Component Pascal),
ein Vorübersetzermakro (C++),
eine Bibliotheksmethode (C#).
Wie bestimmt die Form einer Klassendefinition die Speicher- und Zugriffsarten ihrer
Objekte?
Eine Form der Klassendefinition erlaubt alle Objektarten: Wert- und Referenzobjekte im globalen Speicher und im Laufzeitkeller, Zeigerobjekte auf der Halde
(C++).
Eine Form der Klassendefinition erlaubt Wert- und Zeigerobjekte, die andere nur
Zeigerobjekte (Component Pascal).
Zwei gegenseitig beerbbare Formen der Klassendefinition erlauben Wert- und Referenzobjekte (Eiffel).
Eine Form der Klassendefinition erlaubt nur Referenzobjekte (Java).
Zwei Formen der Typdefinition erlauben entweder Referenzobjekte und Vererbung
oder Wertvariablen und keine Vererbung (C#).
2.4
Aufgaben
Aufgabe 2.6
Code verbessern
Nennen Sie von Programm 2.40 verletzte Programmierleitlinien und schreiben Sie die
C++-Klasse für Uhrzeit so um, dass sie ihre Invariante garantieren kann und die
genannten Leitlinien erfüllt!
Programm 2.40
C++: Schwache
Schnittstelle
und
class Time {
public:
int stnd, minut;
Implementation
void setStunde (int neue_Hour) {stnd=neue_Hour;}
void setze_Min (int am) {minut=am;}
void tick () {(minut=++minut%60)?0:(stnd=++stnd%24);}
};
Hinweis: Die Aufgabe war in der Prüfung Informatik 3 im SS 2007 14 Punkte wert.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.4 Aufgaben
2 – 39
Zusatzfrage: Welche der genannten Programmierleitlinien lassen sich in den anderen
Auswahlsprachen auch verletzen, welche nicht?
Aufgabe 2.7
Datum
Entwerfen, spezifizieren und implementieren Sie eine Klasse für ein Tagesdatum wie
25.10.2007.
Sprachen: Alle.
Aufgabe 2.8
Englischkenntnisse
nachweisen
Übersetzen Sie das folgende Zitat aus [Mey09] p. xxxvi ins Deutsche!
„Also contributing to the difficulties of using Java in an introductory course are the
liberties that the language takes with object-oriented principles. For example: If x
denotes an object and a one of the attributes of the corresponding class, you may by
default write the assignment x.a = v to assign a new value to the a field of the
object. This violates information hiding and other design principles. To rule it out,
you must shadow every attribute with a "getter" function. For the teacher, the choice
is between forcing students early on to add such noise to their programs, or let them
acquire bad design habits which are then hard to unlearn.“
Hinweis: Die Aufgabe war in der Prüfung Informatik 3 im WS 2009/10 10 Punkte wert.
Aufgabe 2.9
Problem schematisch
lösen
Eine Zeichenkette kann einen arithmetischen Ausdruck enthalten. Ausdrücke lassen
sich auswerten. Das Prüfen der Ausdruckssyntax und das Auswerten eines Ausdrucks
sind etwa gleich aufwändig (vgl. PÜ1 Projektübungsblatt: Interpretierer = Zerteiler +
Besucher).
Skizzieren Sie zum Problem „Versuch, zu einer Zeichenkette, die einen Ausdruck enthalten kann, den Wert des Ausdrucks zu erhalten“ Entwürfe der Kunde-Lieferant-Protokolle nach den Programmierschemas
(1)
(2)
(3)
(4)
prozedurales Ein-/Ausgabeschema mit Ergebnisparameter
objektorientiertes Abfrage-Abfrage-Schema
funktionales Schema mit Ausnahme
Ein-/Ausgabeschema mit Ergebnisrückgabe
in dokumentiertem Pseudocode, lieferantenseitig mit Vereinbarungen geeigneter
Dienste, kundenseitig mit Anweisungen, die diese Dienste benutzen! Nennen Sie die
jeweils benötigten Sprachkonstrukte!
Hinweis: Die Ausdruckssyntax könnte durch EBNF-Regeln wie
Expression
Operator
Factor
digit
= Factor { Operator Factor }.
= "+" | "-" | "*" | "/".
= digit { digit } | "(" Expression ")".
= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9".
beschrieben sein. Für die Lösung der Aufgabe ist dies belanglos, da hier eine Schnittstelle zu entwerfen, keine Implementation zu codieren ist.
Aufgabe 2.10
Chaostheorie –
Feigenbaums
Periodenverdopplung
Die Chaostheorie befasst sich mit dynamischen Systemen, deren Verhalten empfindlich von Anfangsbedingungen abhängt. Solch ein System erscheint als „unstetig“,
„ungeordnet“, „unvorhersagbar“, obwohl es deterministisch abläuft und die Beziehungen seiner Elemente oder Zustände durch einfache Regeln oder Gleichungen
beschreibbar sind.
Ein feines kleines Beispielsystem des Physikers Mitchell Feigenbaum ist durch zwei
reelle Zahlen beschrieben. Es bezeichnet
s ∈ (0, 1)
p ∈ (0, 4)
den Zustand,
einen Parameter.
Ausgehend von einem Anfangszustand s0 ∈ (0, 1) ergeben sich Folgezustände durch
si := p ∗ si−1 * (1 − si−1)
27.9.12
für i = 1, 2, 3,...
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 40
2 Einmal quer durch die Sprachen
Entwerfen und implementieren Sie ein Java-Applet, mit dem Sie das Verhalten der
Folge (si) abhängig von s0 und p untersuchen und textuell und grafisch darstellen können! Dokumentieren Sie die Ergebnisse Ihrer Untersuchung!
Verhalten meint: Konvergiert die Folge, oszilliert sie periodisch zwischen mehreren
Werten, oder entwickelt sie sich aperiodisch ohne erkennbare Häufungspunkte? Man
kann analytisch bestimmen, für welche p die Folge gegen welchen Grenzwert konvergiert. Was zeigt Ihr Applet? Warum? Erkunden Sie Bedingungen für Konvergenz und
Formeln für Grenzwerte und validieren Sie sie simulativ mit Zusicherungen!
Interessante Parameterwerte sind p = 3, 3.45, 3.55, 3.57, 3.63, 3.84, 3.8568 usw.
Ändern Sie p um sehr kleine Beträge! Das Beispiel veranschaulicht Feigenbaums
Kaskade der Periodenverdopplung, die bei manchen physikalischen und biologischen Erscheinungen zu beobachten ist. Es gibt einen Wert für p, bei dem Periodizität
in Chaos umschlägt. Doch auch im Chaosbereich tritt Periodizität auf – genau betrachtet kommt sogar jede Periode vor.
Sprache: Java.
Hinweis: Die Aufgabe stammt aus dem Übungsblatt für Wiederholer von Informatik 1
Praktikum im SS 1998. Die Musterlösung findet sich in BlackBox(Hug) in I1Chaos1.
Die folgenden Feigenbaum-Diagramme zeigen die Konvergenz- und Häufungspunkte
der Folge (si) für verschiedene Anfangszustände s0 und Parameterwerte p. Die waagrechte x-Achse entspricht einem Teilintervall des Parameterbereichs [0, 4), die senkrechte y-Achse den Häufungspunkten von (si) im Intervall (0, 1).
Bild 2.4 Feigenbaum-Diagramme
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
2.5 Lösungen
2 – 41
2.5
Lösungen
Lösung zu
Aufgabe 2.8
Eine Übersetzung des Zitats aus Bertrand Meyers Programmiergrundlagenbuch lautet:
„Zu den Schwierigkeiten, Java in einem Einführungskurs einzusetzen, tragen auch
die Freiheiten bei, die sich die Sprache bei objektorientierten Prinzipien herausnimmt. Zum Beispiel: Wenn x ein Objekt bezeichnet und a eines der Attribute der
zugehörigen Klasse, dann kann man standardmäßig (by default) die Zuweisung
x.a = v schreiben, um dem Feld a des Objekts einen neuen Wert zuzuweisen. Das
verletzt das Geheimnisprinzip (information hiding) und andere Entwurfsprinzipien.
Um dies auszuschließen, muss man jedes Attribut durch eine Lesezugriffsfunktion
("getter" function) verschleiern. Der Lehrer steht vor der Alternative: Entweder
zwingt er seine Studenten schon früh dazu, ihren Programmen solches Rauschen
hinzuzufügen, oder er lässt sie sich Entwurfsmarotten angewöhnen, die sie sich nur
schwer wieder abgewöhnen.“
Bemerkung: Mit „standardmäßig“ ist der Schutzzustand gemeint, der entsteht, wenn
das Attribut ohne Schutzmodifikator (also ohne public, protected, private) vereinbart ist. Das Attribut wird dann an alle Klassen im selben Paket exportiert. Allerdings
sind in Java public und protected bei Attributen genau so gefährlich. Meyers Kritik
trifft auch auf public-Attribute in C++ und C# zu und auf Variablen in Component Pascal, die mit der Exportmarke * versehen sind.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 – 42
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
2 Einmal quer durch die Sprachen
27.9.12
3
Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Aufgabe
3dieser
Bild
3 Wurm
3 (3)an Würmern
Leitlinie
MathAussagen
Programm
3 litt, 3 3
DaßBeispiel
Tabelle 3
Die wiederum an Würmern litten –
Joachim Ringelnatz (1883 – 1934)
Der Bandwurm
The extent to which the program correctness can be established is not purely a function of the program’s external specifications and behaviour but depends critically
upon its internal structure.
Edsger W. Dijkstra (1930 – 2002)
Notes on Structured Programming (1972)
A major function of the structuring of the program is to keep a correctness proof
feasible.
David Gries
On Structured Programming – A Reply to Smoliar (1974)
Kleine Kinder haben nicht nur eine große Fähigkeit zur Abstraktion, sondern geradezu einen Instinkt. Denn genau darum geht es beim Erlernen von Sprachen, etwas,
was sämtliche Kleinkinder mit Leichtigkeit bewältigen.
Keith Devlin
Das Mathe-Gen (2003)
Dieses Kapitel variiert Euklids berühmten Algorithmus exemplarisch in verschiedenen
Programmierstilen und -sprachen, gibt Umformungsschemas an, um eine Lösungsvariante in eine andere zu überführen, und diskutiert Eigenschaften der Varianten wie Verständlichkeit, Verifizierbarkeit, Effizienz und Abstraktionsgrad. Drei Abschnitte befassen sich mit
funktionalen,
einfachen objektorientierten, und
abstrakten objektorientierten
Lösungsvarianten.
3.1
Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
Ganze Zahlen, Teilbarkeit und größter gemeinsamer Teiler (greatest common divisor)
ggT : 2 → lN0 , (p, q) |→ ggT(p, q) = max{r ∈ lN0 | r teilt p ∧ r teilt q}
zweier Ganzzahlen behandeln die Lehrmodule Theoretische Grundlagen.1 Bekanntlich
gelten für ganze Zahlen p, q ∈ die Rechenregeln
(3.1)
ggT(p, 0) = |p|,
(3.2)
ggT(p, q) = ggT(q, p),
(3.3)
q ≠ 0 ⇒ ggT(p, q) = ggT(p mod q, q).
1
Karlheinz Hug: Theoretische Grundlagen. Skript. Hochschule Reutlingen, Fak. Informatik,
mki-B (2004) Abschnitt 5.2.3, S. 5-5ff.
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 3 – Seite 1 von 60
3–2
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Daraus erhält man die Rekursionsformel
(3.4)
⎧p
ggT(p, q) = ⎨
⎩ggT (q, p mod q)
falls q = 0
sonst
,
aus der man den rekursiven euklidischen Algorithmus zur Berechnung des größten
gemeinsamen Teilers zweier ganzer Zahlen entwickelt.1 Dieser Algorithmus ist nicht
nur über 2000 Jahre alt, sondern immer noch ein lehrreiches Beispiel und wichtig in
heutigen Anwendungen, z.B. bei Verschlüsselungsverfahren, die auf der Zahlentheorie
beruhen und den größten gemeinsamen Teiler effizient berechnen müssen. Das ggTProblem lautet also:
Problem
Gegeben:
Zwei ganze Zahlen.
Gesucht:
Der größte gemeinsame Teiler der beiden Zahlen.
Entwurf
In Programmiersprachen lässt sich der größte gemeinsame Teiler am besten als Funktion ggT, GCD, Gcd oder gcd realisieren. Wie ist die ggT-Funktion, ein rein funktionales Programmteil, von der Benutzungsoberfläche aus zu benutzen? Der zweite Teil der
Aufgabe ist, einen kleinen ggT-Rechner zu bauen: ein Ein-/Ausgabe-Programmteil mit
einem Aufruf der ggT-Funktion, das zwei Ganzzahlen einnimmt und ihren größten
gemeinsamen Teiler ausgibt. Wir beachten die von Leitlinie LS.14 des Dokuments LS
Leitlinien zum Softwareentwurf geforderte Trennung von Funktion und Ein-/Ausgabe,
ordnen diesen Aspekten bei der ersten Variante zwei Routinen zu, belassen diese aber
der Einfachheit halber in derselben Übersetzungseinheit.
3.1.1
Funktionale Sprachen
Bevor wir Lösungen in den Auswahlsprachen studieren, nutzen wir die rekursive ggTFunktion als Beispiel für den in 1.2.2 S. 1-9 beschriebenen funktionalen Programmierstil, der sich so charakterisieren lässt:
Funktionales
Programmieren
Alle Berechnungen beruhen auf Funktionen und Ausdrücken. Es gibt keine Variablen mit Speicherplätzen, keine Zuweisungen und keine Schleifen, aber bedingte
Ausdrücke und rekursive Funktionsaufrufe.
Dazu betrachten wir einige Funktionsdefinitionen und -anwendungen für den größten
gemeinsamen Teiler zweier Ganzzahlen in funktionalen Sprachen. Zunächst transformieren wir die Rekursionsformel (3.4) in eiffelischen funktionalen Pseudocode.
Programm 3.1
Funktionaler
Pseudocode: Größter
gemeinsamer Teiler
-- Definition:
gcd (p, q : INTEGER) : INTEGER is
if q = 0 then
abs (p)
else
gcd (q, p mod q)
end
-- Application:
gcd (12, 34)
Das Konstrukt
Bedingter Ausdruck
if Ausdruck1 then Ausdruck2 else Ausdruck3 end
1
Euklid von Alexandria (um 365 – 300 v.u.Z.), griechischer Mathematiker, Physiker, schuf
Grundlegendes zu Geometrie, Zahlentheorie, Aufbau und Beweismethodik der Mathematik.
Den nach ihm benannten Algorithmus beschreibt er in Elemente, Buch VII in Satz 1 und 2. Übrigens wird in der Literatur oft schlicht vergessen, die Berechnung des ggT(p, q) für negatives p
oder q korrekt anzugeben.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
3–3
ist ein bedingter Ausdruck (conditional expression) mit der Semantik: Ist der boolesche Ausdruck1 wahr, so wird Ausdruck2, sonst Ausdruck3 ausgewertet und der erhaltene
Wert als Wert des bedingten Ausdrucks eingesetzt. Dazu müssen Ausdruck2 und
Ausdruck3 vom selben Typ sein.
Programm 3.2
Scheme: Größter
gemeinsamer Teiler
; Definition:
(define (gcd p q)
(if (= q 0)
(abs p)
(gcd q (remainder p q))
)
)
; Application:
(gcd 12 34)
Scheme benutzt konsequent kommalose Listen- und Präfixnotation und verzichtet auf
syntaktischen Zucker. Im Unterschied zum dynamisch typisierten Scheme ist ML statisch typisiert mit pascalischer Syntax.
Programm 3.3
ML: Größter
gemeinsamer Teiler 1
(* Definition: *)
fun gcd (p, q) : int =
if q = 0 then
abs (p)
else
gcd (q, p mod q);
(* Application: *)
gcd (12, 34)
ML ermöglicht auch Funktionen mit alternativen Mustern zu definieren, wobei der
senkrechte Strich „|“ die Muster trennt.
Programm 3.4
ML: Größter
gemeinsamer Teiler 2
(* Definition: *)
fun gcd (p, 0) : int = abs (p)
|
gcd (p, q) : int = gcd (q, p mod q);
Ähnlich ML bietet Miranda eine Definitionsform mit Mustern.
Programm 3.5
Miranda: Größter
gemeinsamer Teiler 1
|| Definition:
gcd (p, 0) = abs (p)
gcd (p, q) = gcd (q, p mod q)
|| Application:
gcd (12, 34)
Eine weitere Definitionsform von Miranda, die mit rechts vom definierenden Ausdruck
stehenden Wächtern (guard), d.h. Anwendbarkeitsbedingungen arbeitet, ähnelt noch
mehr der Rekursionsformel (3.4).
Programm 3.6
Miranda: Größter
gemeinsamer Teiler 2
|| Definition:
gcd (p, q) = abs (p),
if q = 0
gcd (p, q) = gcd (q, p mod q), otherwise
Was lehren die Programme 3.1 bis 3.6? Erstens, dass man zunächst eine korrekte
Lösung eines Problems kennen muss, hier die Rekursionsformel (3.4). Zweitens, dass
es nachrangig ist, in welcher Sprache man die Lösung notiert.
Die Kenntnis funktionaler Programmiertechniken ist auch für imperatives Programmieren nützlich. In imperativen Sprachen, die ein hinreichend allgemeines Funktionskonzept unterstützen, kann man bestimmte Probleme in funktionalem Programmierstil
lösen. Dies trifft z.B. auf die hybriden Sprachen Component Pascal und C++ zu.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3–4
Logisches
Programmieren
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Kurz abschweifend werfen wir einen Blick auf eine logikbasierte Problemlösung in
Prolog, die statt der Funktion ggT eine passende Aussage fordert. Die Gleichung
ggT(p, q) = r
erfüllt diesen Zweck; es ist nur ein Prädikat dafür zu definieren.
Programm 3.7
Prolog: Größter
gemeinsamer Teiler
% Rules:
euclid (p, 0, p)
:- p >= 0
euclid (p, 0, gcd)
:- p < 0, gcd = -p
euclid (p, q, gcd)
:- q \= 0,
qn is p mod q,
euclid (q, qn, gcd)
% Goal:
? :- euclid (12, 34, x)
3.1.2
Component Pascal
Vom Ausdruck zur
Anweisung
Wir formulieren zunächst eine rekursive Variante der ggT-Funktion. Programm 3.8 ist
in einer imperativen Sprache geschrieben und daher nicht rein funktional: Component
Pascal kennt keine bedingten Ausdrücke, nur bedingte Anweisungen. Statt der alternativen Ausdrücke sind alternative RETURN-Anweisungen zu notieren, bei denen freilich
nicht das Schlüsselwort RETURN wesentlich ist, sondern die Ausdrücke, deren Werte
zurückgegeben werden. Streicht man die RETURNs, so wandelt sich die bedingte
Anweisung zum bedingten Ausdruck.
Programm 3.8
Component Pascal:
Größter gemeinsamer
Teiler, rekursiv
PROCEDURE GCD* (p, q : INTEGER) : INTEGER;
(*!
Greatest Common Divisor of p and q.
Implemented by Euclid's algorithm, recursive.
!*)
BEGIN
IF q = 0 THEN
RETURN ABS (p);
ELSE
RETURN GCD (q, p MOD q);
END;
END GCD;
Es geht auch ohne ELSE-Zweig:
Programm 3.9
Component Pascal:
Größter gemeinsamer
Teiler, rekursiv
Verständlich kontra
kurz und unstrukturiert
PROCEDURE GCD* (p, q : INTEGER) : INTEGER;
BEGIN
IF q = 0 THEN
RETURN ABS (p);
END;
RETURN GCD (q, p MOD q);
END GCD;
In Programm 3.8 treffen die durch die RETURN-Anweisungen bewirkten dynamischen
Enden auf das statische Ende der Funktion. Deshalb mag dieser Algorithmus als strukturiert gelten. In Programm 3.9 bildet die erste RETURN-Anweisung im THEN-Zweig
ein dynamisches Ende, das sich vom statischen Ende unterscheidet, da eine weitere
Anweisung folgt, die im Fall q # 0 ausgeführt wird. Deshalb ist dieser Algorithmus
unstrukturiert (er ist nicht mit einem Struktogramm darstellbar). Setzt man ihn unbedarft in Sprachen mit syntaktischen Schwächen um, können Fehler entstehen. Während
Programm 3.8 die Alternativen mit der IF-THEN-ELSE-Struktur visualisiert, verschleiert
Programm 3.9 die zweite Alternative nach der einseitigen Auswahl als Sequenz – dass
es eine Alternative ist, wird erst beim Lesen des THEN-Zweigs klar. Fazit: Programm
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
3–5
3.9 ist schwerer verständlich als Programm 3.8. Deshalb sollte man auf fragwürdige
Einsparungen von ELSE-Zweigen auf Kosten guter Struktur verzichten.
Rekursiv kontra iterativ
Rekursive Problemlösungen sind elegant. Nachteilig an ihnen ist jedoch der erhöhte
Laufzeitaufwand für die rekursiven Routinenaufrufe durch die damit notwendige Verwaltung des Laufzeitkellers, manchmal auch der erhöhte Speicheraufwand, der den
Laufzeitkeller erschöpfen kann. In der imperativen Programmierung bevorzugt man
daher effizientere iterative Lösungen – sofern der Verlust an Eleganz vertretbar ist.
Zu jedem rekursiven Algorithmus gibt es einen funktional äquivalenten iterativen
Algorithmus und umgekehrt, d.h. Rekursion und Iteration sind gleich problemlösungsmächtig. Jeder rekursive Algorithmus lässt sich durch Simulation des Laufzeitkellers in
eine iterative Form bringen. Allerdings verlängern Kellerroutinenaufrufe die Laufzeit;
expandierte Aufrufe verkürzen die Laufzeit nur wenig. Daher interessieren hinsichtlich
Effizienz iterative Varianten rekursiver Algorithmen, die ohne Keller auskommen. Zu
manchen rekursiven Einzellösungen existieren effiziente iterative Einzellösungen. Gibt
es darüber hinaus Umformungsschemas, die sich auf Kategorien rekursiver Lösungen
anwenden lassen? Ja, unter gewissen Bedingungen lassen sich rekursive Algorithmen
schematisch in funktional äquivalente iterative Algorithmen ohne Keller umformen.
Endrekursion
Eine solche Bedingung trifft in Programm 3.8 (und Programm 3.9) zu: Der rekursive
Aufruf von GCD steht in einer dynamisch letzten Anweisung der Funktion als Ausdruck, der als letzter vor dem Rücksprung ausgewertet wird; damit handelt es sich um
einen endrekursiven Aufruf (rechtsrekursiv, tail recursive call). Eine Funktion oder
allgemein Routine, bei der alle rekursiven Aufrufe endrekursiv sind, heißt endrekursiv. Endrekursion (Rechtsrekursion, tail recursion) lässt sich schematisch durch Iteration ersetzen.1 Für Funktionen mit schreibbaren Eingabeparametern lautet das Umformungsschema für endrekursive Algorithmen in iterative Algorithmen:
Regel 3.1
Umformung
endrekursiver
Algorithmen
(1)
Vereinbare für jeden Formalparameter fp1,.., fpn der rekursiven Funktion eine
lokale Variable lv1,.., lvn.
(2)
Weise vor jedem rekursiven Aufruf den lokalen Variablen die entsprechenden
Aktualparameter zu:
lv1 := ap1;..; lvn := apn.
(3)
Weise nach jedem rekursiven Aufruf den Formalparametern die entsprechenden lokalen Variablen zu:
fp1 := lv1;..; fpn := lvn.
(4)
(5)
Umgebe den Anweisungsteil mit einer unbedingten Schleife.
Streiche die rekursiven Aufrufe.
Beweis. Zu zeigen ist die Korrektheit des Schemas, d.h. seine Eigenschaft, jeden endrekursiven Algorithmus in einen äquivalenten iterativen Algorithmus zu überführen.
Dazu genügt zu zeigen, dass jeder der fünf Schritte funktionserhaltend ist. Offenbar
sind die Schritte (1) bis (3) funktionserhaltend, denn sie ergänzen den rekursiven Algorithmus um Vereinbarungen und Zuweisungen, die nichts am Ergebnis ändern. Ist das
Sprachkonstrukt zum Verlassen der Funktion eine RETURN-Anweisung wie in Component Pascal oder den C*-Sprachen, so ist auch Schritt (4) funktionserhaltend, da
RETURN aus der scheinbaren Endlosschleife springt. (Sprachen ohne RETURN-Anweisung wie Eiffel fordern hier etwas mehr Überlegung.)
Schritt (5) scheint auf den ersten Blick die Funktion zu ändern, aber beim zweiten
Blick sieht man, dass die zusätzlichen lokalen Variablen dieselben Werte erhalten wie
die Aktualparameter des folgenden rekursiven Aufrufs und damit die Formalparameter,
1
27.9.12
Siehe [Lou93] S. 364, [Set97] S. 188, [GoZ06] 2. Auflage, S. 68.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3–6
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
die zunächst in der nächsten Schachtel auf dem Laufzeitkeller liegen. Vor dem Streichen des rekursiven Aufrufs sind die folgenden Zuweisungen unerreichbar, nach dem
Streichen kopieren sie die Werte der lokalen Variablen in die Formalparameter des
aktuellen Aufrufs. Dabei werden die alten Werte der Formalparameter überschrieben,
was aber nichts an der Funktion ändert, da diese Werte hinter den endrekursiven Aufrufen nicht mehr benötigt werden. Die Formalparameter erhalten also iterativ dieselben
Werte, die die Formalparameter der rekursiven Aufrufe erhalten hätten.
Programm 3.8 mit Regel 3.1 transformiert ergibt Programm 3.10.
Programm 3.10
Component Pascal:
Größter gemeinsamer
Teiler, umgeformt
PROCEDURE GCD* (p, q : INTEGER) : INTEGER;
VAR
lp, lq : INTEGER;
BEGIN
LOOP
IF q = 0 THEN
RETURN ABS (p);
ELSE
lp := q;
lq := p MOD q;
RETURN GCD (q, p MOD q);
p := lp;
q := lq;
END;
END;
END GCD;
Prinzipiell kann ein Kompilierer Endrekursionen nach diesem Schema in Schleifen
umformen. Tut er es, kann der Programmierer ohne Effizienzverlust endrekursiv codieren. Beispielsweise verlangt die Scheme-Sprachdefinition von Scheme-Implementationen, dass sie echt endrekursiv sind, d.h. unbeschränkt viele aktive Endaufrufe erlauben
(z.B. realisierbar durch Schleifen).1
Hier interessiert uns: Lässt sich das schematisch erhaltene iterative Programm 3.10 vereinfachen? Ja – die unbedingte Schleife ergibt mit der Auswahlanweisung im Schleifenrumpf eine kopfgesteuerte Bedingungsschleife. Eine lokale Variable ist verzichtbar
(z.B. lp), die zweite benennen wir um (z.B. lq in r).
Programm 3.11
Component Pascal:
Größter gemeinsamer
Teiler, vereinfacht
PROCEDURE GCD* (p, q : INTEGER) : INTEGER;
VAR
r : INTEGER;
BEGIN
WHILE q # 0 DO
r := p MOD q;
p := q;
q := r;
END;
RETURN ABS (p);
END GCD;
Eine weitere kleine Änderung soll die Lesbarkeit verbessern und Probleme vermeiden,
die bei negativen Moduluswerten auftreten können, wenn die Modulooperation in der
Sprache nicht mathematisch korrekt definiert oder in der Sprachimplementation nicht
korrekt realisiert ist (was auf Component Pascal/BlackBox nicht zutrifft). Die Änderung beruht auf der Rechenregel
1
Siehe „5.11. Proper tail recursion“ in: M. Sperber, R. K. Dybvig, M. Flatt, A. Straaten (Eds):
Revised6 Report on the Algorithmic Language Scheme. (26. Sept. 2007), S. 20, http://
www.r6rs.org/final/r6rs.pdf (Zugriff 2011-05-18).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
3–7
ggT(p, q) = ggT(−p, q) = ggT(p, −q) = ggT(−p, −q).
(3.5)
Entfernten die bisherigen ggT-Varianten das Minuszeichen erst am Ergebnis, so entfernt Programm 3.12 die Minuszeichen schon an den Eingangswerten.
Programm 3.12
Component Pascal:
Größter gemeinsamer
Teiler, iterativ
Effizient kontra klar
PROCEDURE GCD* (p, q : INTEGER) : INTEGER;
(*!
Greatest Common Divisor of p and q.
Implemented by Euclid's algorithm, iterative without a selection statement.
!*)
VAR
r : INTEGER;
BEGIN
p := ABS (p);
q := ABS (q);
WHILE q > 0 DO
r := p MOD q;
p := q;
q := r;
END;
RETURN p;
END GCD;
Was das iterative Programm 3.12 an Effizienz relativ zum rekursiven Programm 3.8
gewinnt, verliert es an Klarheit durch die zusätzliche lokale Variable und die fünf
Zuweisungen. Eine Fallunterscheidung statt des Ringtauschs erlaubt eine noch effizientere, aber auch weniger klare Lösungsvariante.1 Dagegen lässt sich mit einer kollektiven Zuweisung, d.h. einer Zuweisung, die mehreren Variablen simultan Werte zuweist,
die lokale Variable einsparen und der iterative Algorithmus verkürzen. Programm 3.13
zeigt das aus Algol 68 bekannte Sprachkonstrukt der kollektiven Zuweisung im Kontext von Python, das allerdings eine dynamisch typisierte Sprache ist, sodass dieser
Lösungsvariante statische Typsicherheit fehlt. Zudem ist angenommen, dass Python
den Modulooperator mathematisch korrekt definiert.
Programm 3.13
Python: Größter
gemeinsamer Teiler mit
kollektiver Zuweisung
def gcd (p, q):
"""Greatest Common Divisor of p and q.
Implemented by Euclid's algorithm, iterative with parallel assignments."""
while q != 0:
p, q = q, p % q
return abs(p)
Die kollektive Zuweisung als Vektorzuweisung interpretiert motiviert dazu, das in
Regel 3.1 verbalisierte Umformungsschema alternativ in Regel 3.2 mit vereinfachten,
formalisierten, austauschbaren Pseudocodemustern darzustellen. Dabei sind x, r(x) Vektoren, „:=“ die Vektorzuweisung, f(x), b(x), s(x) Funktionen, A(x) eine Anweisungsfolge.
Regel 3.2
Pseudocode:
Umformung eines
endrekursiven
Funktionsmusters in
iterative
Funktionsmuster
f(x) is
if b(x) then return s(x) else A(x); return f(r(x)) end
end
f(x) is
until b(x) loop A(x); x := r(x) end
return s(x)
end
f(x) is
while not b(x) loop A(x); x := r(x) end
return s(x)
end
1
27.9.12
Siehe Component-Pascal-Modul MathArithmeticsI in BlackBox-Informatik-Hug-Subsystemen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3–8
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Nun zum zweiten Teil der Aufgabe: In BlackBox lässt sich der ggT-Rechner am einfachsten durch ein Kommando mit zwei ganzzahligen Parametern realisieren.
Programm 3.14
Component Pascal:
Rechner für größten
gemeinsamen Teiler
MODULE I0GCDCalculator;
IMPORT
StdLog;
PROCEDURE GCD* (p, q : INTEGER) : INTEGER;
...
END GCD;
PROCEDURE Do* (p, q : INTEGER);
BEGIN
StdLog.Open;
StdLog.String ("GCD ("); StdLog.Int (p);
StdLog.String (", ");
StdLog.Int (q);
StdLog.String (") = ");
StdLog.Int (GCD (p, q));
StdLog.Ln;
END Do;
END I0GCDCalculator.
Zu benutzen ist der ggT-Rechner durch einen Kommandoaufruf wie
! "I0GCDCalculator.Do (48, 15)"
Fehleingabe
mit den gewünschten Zahlen. Der BlackBox-Kommandointerpretierer analysiert den
Aufruf und führt ihn aus. Fehleingaben wie falsche Anzahl oder nicht ganzzahlige
Werte von Parametern behandelt der Kommandointerpretierer. Das Kommando braucht
sich nicht um Fehleingaben zu kümmern.
3.1.3
Eiffel
Spezifikation
Eiffel unterstützt Spezifikation durch Vertrag. Damit können wir die ggT-Funktion
zuerst mit Nachbedingungen spezifizieren, die der Rekursionsformel (3.4) entsprechen.
Programm 3.15
Eiffel: Größter
gemeinsamer Teiler,
spezifiziert
abs
Programm 3.16
Eiffel: Größter
gemeinsamer Teiler,
rekursiv
gcd (p, q : INTEGER) : INTEGER
-- Greatest common divisor of p and q.
ensure
direct_result:
q = 0 implies Result = p.abs
indirect_result:
q /= 0 implies Result = gcd (q, p \\ q)
abs ist eine Abfrage der INTEGER-Klasse für den Absolutbetrag.
gcd (p, q : INTEGER) : INTEGER
-- Greatest common divisor of p and q.
-- Implemented by Euclid's algorithm, recursive.
do
if q = 0 then
Result := p.abs
else
Result := gcd (q, p \\ q)
end
ensure
direct_result:
q = 0 implies Result = p.abs
indirect_result:
q /= 0 implies Result = gcd (q, p \\ q)
end -- gcd
Programm 3.16 demonstriert die Verwandtschaft zwischen mathematischer Aussage,
vertraglicher Spezifikation und funktionaler Lösung. Übrigens gilt bei manchen EiffelImplementationen: Auch wenn Prüfungen von Zusicherungen (wie hier Nachbedingungen) zur Laufzeit angeschaltet sind, werden rekursive Aufrufe in Nachbedingungen
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
3–9
nicht ausgeführt. Das vermeidet zwar wiederholte rekursive Aufrufe mit gleichen Parameterwerten, verhindert aber auch, die Korrektheit der Implementation gegen die Spezifikation dynamisch zu prüfen.
Programm 3.17
Eiffel: Größter
gemeinsamer Teiler,
iterativ
Schleifeninvariante und
-variante
gcd (p, q : INTEGER) : INTEGER
-- Greatest common divisor of p and q.
-- Implemented by Euclid's algorithm, iterative.
local
s, t : INTEGER
do
from
Result := p.abs
s := q.abs
invariant
0 <= Result and Result <= p.abs.max (q.abs)
0 <= s and s <= q.abs
gcd (Result, s) = gcd (p, q)
until s <= 0 loop
t := Result \\ s
Result := s
s := t
variant
s
end
end -- gcd
Der iterative euklidische Algorithmus in Programm 3.17 zeigt, wie Eiffel-Konstrukte
Schleifeninvarianten und -varianten unterstützen. Diese sind Mittel der axiomatischen
Semantik, um die Korrektheit von Schleifen zu beweisen. Die von C. A. R. Hoare
begründete axiomatische Semantik beschreibt die Semantik von Anweisungskonstrukten durch Bedingungen, insbesondere Vor- und Nachbedingungen.
Eine Schleifeninvariante (loop invariant) ist eine Zusicherung, die vor und nach
jeder Ausführung des Schleifenrumpfs gilt. Sie gilt also nach der Schleifeninitialisierung, bleibt bei jeder Iteration erhalten, und gilt nach der Schleifenterminierung.
Eine Schleifenvariante (loop variant) ist ein natürlichzahliger Ausdruck, dessen
Wert sich bei jeder Iteration vermindert. Da er nicht negativ werden kann, garantiert
er, dass die Schleife terminiert.
Da Formalparameter schreibgeschützt sind (um vertragliche Spezifikation nicht zu
erschweren), sind die lokalen Variablen s und t zu vereinbaren, um sich ändernde Werte
aufzunehmen. Die Schleifeninvariante
0 <= Result and Result <= p.abs.max (q.abs) and 0 <= s and s <= q.abs
ist offensichtlich, aber schwach. Die interessante Invariante
gcd (Result, s) = gcd (p, q)
entspricht der Rekursionsformel (3.4). Bei angeschalteter Prüfung der Schleifeninvarianten führt sie freilich zu vielen rekursiven Aufrufen. Als Schleifenvariante eignet sich
s, das bei jeder Iteration durch
s := Result \\ s
kleiner wird. Nun sind die Fragmente zum vollständigen Programm eines Kommandozeilen-ggT-Rechners zusammenzustellen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 10
Programm 3.18
Eiffel: Rechner für
größten gemeinsamen
Teiler
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
note
description: "Command-line calculator for greatest common divisor of two integers"
class GCD_CALCULATOR
create
make
feature
gcd (p, q : INTEGER) : INTEGER
-- Greatest common divisor of p and q.
-- Implemented by Euclid's algorithm, iterative.
local
s, t : INTEGER
do
from
Result := p.abs
s := q.abs
invariant
0 <= Result and Result <= p.abs.max (q.abs)
0 <= s and s <= q.abs
gcd (Result, s) = gcd (p, q)
until s <= 0 loop
t := Result \\ s
Result := s
s := t
variant
s
end
ensure
direct_result:
q = 0 implies Result = p.abs
indirect_result:
q /= 0 implies Result = gcd (q, p \\ q)
end -- gcd
make (arguments : ARRAY [STRING])
-- Given two integer command arguments, print their greatest common divisor.
do
if arguments = Void or else arguments.count < 3 or else
not arguments.item (1).is_integer or else
not arguments.item (2).is_integer
then
io.put_string ("Please call the gcd calculator with two integer arguments!")
else
io.put_integer
(gcd (arguments.item (1).to_integer, arguments.item (2).to_integer))
end
io.put_new_line
end -- make
end -- class GCD_CALCULATOR
Auswertung
boolescher Ausdrücke
Fehleingabe
Eine Neuigkeit von Programm 3.18: Eiffel kennt wie Ada zwei Varianten der booleschen Operatoren: Ausdrücke mit and und or werden lang ausgewertet, Ausdrücke mit
and then und or else kurz. Dass hier erst die Anzahl der Argumente zu prüfen ist und
nur ggf. anschließend ihr Typ, erfordert kurze Auswertung mit or else. Alternativ
könnte eine mehrseitige if-elseif-Auswahlanweisung nach verschiedenen Fehleingaben
differenzieren.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
3.1.4
3 – 11
C++
Die C++-Varianten der ggT-Funktionen erhält man leicht aus den Component-PascalVarianten durch rein syntaktische Änderungen.
Programm 3.19
C++: Größter
gemeinsamer Teiler,
rekursiv
abs
Bedingter Ausdruck
int gcd (int p, int q) {
if (q == 0) {
return abs(p);
} else {
return gcd(q, p % q);
}
}
Für die abs-Funktion ist cstdlib.h zu inkludieren. Ersetzen wir in der rekursiven Variante
die bedingte Anweisung durch einen bedingten Ausdruck, der in C* mit dem ternären
?:-Operator die Form
Bedingung ? Wahrausdruck : Falschausdruck
annimmt, so erhalten wir eine fast rein funktionale Variante, die schön kurz ist; nur das
Falltrennzeichen „:“ ist etwas unscheinbar.
Programm 3.20
C++: Größter
gemeinsamer Teiler,
fast funktional
Historisches
Programm 3.21
C++: Größter
gemeinsamer Teiler,
iterativ
int gcd (int p, int q) {
return q == 0 ? abs(p) : gcd(q, p % q);
}
Exkurs. Bedingte Ausdrücke und Rekursion, Konzepte des funktionalen Programmierparadigmas, wurden mit Algol 60 in die imperative Programmierwelt eingeführt und fanden über
Algol 68 den Weg nach C und C*.
int gcd (int p, int q) {
int r;
p = abs(p);
q = abs(q);
while (q > 0) {
r = p % q;
p = q;
q = r;
}
return p;
}
Bedingungsschleife
Das iterative Programm 3.21 zeigt die kopfgesteuerte while-Bedingungsschleife der
C*-Sprachen, die im Wesentlichen wie in Component Pascal funktioniert. Unterschied
ist, dass im Schleifenrumpf nur eine Anweisung stehen darf und die Schleifenanweisung (wie fast alle strukturierten Anweisungen der C*-Sprachen) nicht durch ein
Schlüsselwort abgeschlossen ist. Daraus resultierende Probleme (Unstetigkeitsstellen,
hängende else-Zweige) diskutieren wir anderswo. Mindern kann man sie, indem man
als Rumpf stets eine mit { } geklammerte Blockanweisung einsetzt.
Aufgabe 3.1
C++: Größter
gemeinsamer Teiler,
fehlerhaft
Welcher Fehler ist dem Programmierer in der folgenden Funktion unterlaufen und welche Folgen hat er? Ist die Funktion übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Falls ja, liefert das Laufzeitsystem eine Fehlermeldung? Falls ja,
welche?
int gcd (int p, int q) {
return q = 0 ? abs(p) : gcd(q, p % q);
}
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 12
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Der Kommandozeilen-ggT-Rechner sieht in C++ so aus:
Programm 3.22
C++: Rechner für
größten gemeinsamen
Teiler
// Implementation file: GcdCalculator.cpp
#include <cstdlib>
#include <iostream>
int gcd (int p, int q) {
int r;
p = abs(p);
q = abs(q);
while (q > 0) {
r = p % q;
p = q;
q = r;
}
return p;
}
int main (int argc, char * argv[]) {
if (argc < 3) {
std::cout << "Please call the gcd calculator with two integer arguments!";
} else {
std::cout << gcd(std::atoi(argv[1]), std::atoi(argv[2]));
}
std::cout << std::endl;
return 0;
}
Fehleingabe
Im Unterschied zum Eiffel-Programm 3.18 beantwortet das C++-Programm 3.22 nur
fehlende Argumente mit dem Benutzungshinweis. Bei zwei Argumenten, die beide
keine Ganzzahlen darstellen, ist die Antwort 0; ist eines von zwei Argumenten eine
Ganzzahl, so erscheint diese als Antwort. Diese Fälle als Fehleingaben zu diagnostizieren wäre relativ aufwändig, weshalb wir hier darauf verzichten – dessen bewusst, dass
die Fälle „fehlendes Argument“ und „Argument falschen Typs“ besser einheitlich zu
behandeln wären.
3.1.5
Java
Die Varianten der ggT-Funktion – rekursiv mit bedingter Anweisung oder bedingtem
Ausdruck, oder iterativ – sehen in Java und C# fast so aus wie in C++. Deshalb
beschränken wir uns auf die effizientere iterative Variante. In Java nimmt der Kommandozeilen-ggT-Rechner diese Form an:
Programm 3.23
Java: Rechner für
größten gemeinsamen
Teiler
public class GcdCalculator {
public static int gcd (int p, int q) {
int r;
p = Math.abs(p);
q = Math.abs(q);
while (q > 0) {
r = p % q;
p = q;
q = r;
}
return p;
}
protected static final String HINT =
"Please call the gcd calculator with two integer arguments! ";
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
3 – 13
public static void main (String[] args) {
if (args.length < 2) {
System.out.println("Missing Argument. " + HINT);
} else {
try {
System.out.println
(gcd
(Integer.parseInt(args[0]), Integer.parseInt(args[1])));
} catch (Exception e) {
System.out.println
("Wrong Argument. " + HINT + e.toString());
}
}
}
}
abs
Die abs-Funktion ist eine Klassenfunktion der Standardbibliotheksklasse Math.
Fehleingabe
Um die verschiedenen Fehleingaben zu differenzieren, wird das if-else für die Prüfung vor dem gcd-Aufruf, das try-catch für den Fehlschlag beim gcd-Aufruf benötigt. Die hybride abfrage- und ausnahmeorientierte Java-Lösung ist damit komplexer
als die rein abfrageorientierte Eiffel-Lösung Programm 3.18. Alternativ kann man, um
das if-else zu sparen, aus der Not eine Untugend machen und das Prüfen der Argumentanzahl der obligatorischen dynamischen Indexprüfung überlassen, die ggf. eine
Ausnahme des Typs ArrayIndexOutOfBoundsException auslöst:
Programm 3.24
Java: Ausnahmen bei
Fehleingaben
public static void main (String[] args) {
try {
System.out.println
(gcd(Integer.parseInt(args[0]), Integer.parseInt(args[1])));
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println
("Missing Argument. " + HINT + e.toString());
} catch (NumberFormatException e) {
System.out.println
("Wrong Argument. " + HINT + e.toString());
}
}
Davon rate ich allerdings ab. Mit Ausnahmen sollte man echte Ausnahmefälle behandeln, nicht zwar unpassende, aber durchaus normale Eingaben des Benutzers.
Aufgabe 3.2
Java: Größter
gemeinsamer Teiler,
fehlerhaft
Welcher Fehler ist dem Programmierer in der folgenden Funktion unterlaufen und welche Folgen hat er? Ist die Funktion übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Falls ja, liefert das Laufzeitsystem eine Fehlermeldung? Falls ja,
welche?
int gcd (int p, int q) {
return q = 0 ? Math.abs(p) : gcd(q, p % q);
}
3.1.6
Abs
27.9.12
C#
Die C#-Variante des Kommandozeilen-ggT-Rechners offenbart keine neuen Aspekte.
Die Abs-Funktion ist eine Klassenfunktion der Standardbibliotheksklasse Math im
Namensraum System. Wird die Konversion nach dem funktionalen Schema mit Ausnahme gewählt, so ähnelt die Main-Funktion der der Java-Variante Programm 3.23. Bei
Konversion nach dem Ein-/Ausgabe-Schema mit Ergebnisrückgabe (S. 2-25) wie in
Programm 3.25 nähert sich die Struktur von Main der Eiffel-Variante Programm 3.18.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 14
Programm 3.25
C#: Rechner für
größten gemeinsamen
Teiler
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
public class GcdCalculator {
public static int Gcd (int p, int q) {
int r;
p = System.Math.Abs(p);
q = System.Math.Abs(q);
while (q > 0) {
r = p % q;
p = q;
q = r;
}
return p;
}
protected const string Hint =
"Please call the gcd calculator with two integer arguments!";
public static void Main (string[] args) {
if (args.Length < 2) {
System.Console.WriteLine("Missing Argument. " + Hint);
} else {
int p, q;
if (System.Int32.TryParse(args[0], out p) &&
System.Int32.TryParse(args[1], out q)) {
System.Console.WriteLine(Gcd(p, q));
} else {
System.Console.WriteLine("Wrong Argument. " + Hint);
}
}
}
}
Aufgabe 3.3
C#: Größter
gemeinsamer Teiler,
fehlerhaft
Welcher Fehler ist dem Programmierer in der folgenden Funktion unterlaufen und welche Folgen hat er? Ist die Funktion übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Falls ja, liefert das Laufzeitsystem eine Fehlermeldung? Falls ja,
welche?
int Gcd (int p, int q) {
if (q == 0);
return System.Math.Abs(p);
return gcd(q, p % q);
}
3.1.7
Fazit
Die ggT-Funktion lässt sich als Funktion der Programmiersprache
vertraglich spezifizieren (Eiffel),
rekursiv und iterativ implementieren (Component Pascal, Eiffel, C*).
Fehleingaben beim Aufruf des ggT-Rechners behandelt
der Kommandointerpretierer der Sprachumgebung (Component Pascal/BlackBox),
die aufgerufene Prozedur (Eiffel, C*).
Die Anzahl der Argumente wird geprüft durch eine Abfrage (Eiffel, C*). Dass sich ein
Zeichenkettenargument nicht in eine Ganzzahl konvertieren lässt, wird
vorher durch eine Abfrage geprüft (Eiffel),
durch Rückgabe
von 0 als Misserfolgswert (C++), oder
eines Ergebniswerts (C#) angezeigt,
löst eine Ausnahme aus (Java, C#).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional
3.1.8
Aufgaben
Aufgabe 3.4
ggT- und kgV-Rechner
(1)
(2)
(3)
3 – 15
Testen Sie den Kommandozeilen-ggT-Rechner nach 3.1 mit rekursiven und iterativen ggT-Funktionsvarianten!
Wenden Sie das Schema zur Umformung endrekursiver in iterative Algorithmen
auf die rekursive ggT-Funktion in Eiffel und C* an!
Ergänzen Sie das ggT-Programm um
eine Funktion für das kleinste gemeinsame Vielfache (kgV; least common
multiple, lcm),
die Ausgabe des kgV der eingegebenen Zahlen!
Sprachen: Alle.
Aufgabe 3.5
ggT-Funktion iterativ
variiert
Programm 3.26
C++: ggT-Funktion 1
Programm 3.27
C++: ggT-Funktion 2
Programm 3.28
C++: ggT-Funktion 3
Programm 3.29
C++: ggT-Funktion 4
27.9.12
In den C*-Sprachen bietet die for-Schleife viele Möglichkeiten, die Schreibung desselben iterativen euklidischen Algorithmus zu variieren. Vier sind unten mit der whileVariante in C++ formuliert. Überlegen Sie sich Kriterien zur Bewertung der fünf Notationsvarianten, bewerten und ordnen Sie die Varianten neu von 1 (beste) bis 5
(schwächste) und begründen Sie die Ordnung!
int gcd1 (int p, int q) {
int r;
p = abs(p);
q = abs(q);
while (q > 0) {
r = p % q;
p = q;
q = r;
}
return p;
}
int gcd2 (int p, int q) {
for (int r, p = abs(p), q = abs(q); q > 0; ) {
r = p % q;
p = q;
q = r;
}
return p;
}
int gcd3 (int p, int q) {
for (int r, p = abs(p), q = abs(q); q > 0; q = r) {
r = p % q;
p = q;
}
return p;
}
int gcd4 (int p, int q) {
for (int r, p = abs(p), q = abs(q); q > 0; p = q, q = r)
r = p % q;
return p;
}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 16
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Programm 3.30
C++: ggT-Funktion 5
int gcd5 (int p, int q) {
for (int r, p = abs(p), q = abs(q); q > 0; r = p % q, p = q, q = r);
return p;
}
Hinweis: Die Aufgabe war Teil der Prüfung Informatik 3 im WS 2004/05.
Sprachen: C*.
Aufgabe 3.6
Stellenzahl
(1)
Programmieren Sie die Funktion
number_of_digits (n : INTEGER) : INTEGER
-- Number of digits of the decimal representation of n.
(2)
die die Anzahl der Dezimalziffern ihres Parameters n liefert (Beispiel:
number_of_digits (123) = 3), mit einem
(a) rekursiven,
(b) iterativen
Algorithmus! (Aufgabe (b) wurde auch in Informatik 1 Praktikum gestellt.) Falls
die rekursive Variante endrekursiv ist, wenden Sie auf sie das Schema zur
Umformung endrekursiver in iterative Algorithmen an! Sonst überlegen Sie sich
eine Methode, nach der sich die Funktion in eine endrekursive Funktion umformen lässt, sodass Sie den ersten Fall anwenden können!1
Erweitern Sie die Varianten aus (1) um einen zweiten Parameter base für die
Basis der Darstellung von n:
number_of_digits (n, base : INTEGER) : INTEGER
-- Number of digits of the representation of n with respect to the base base.
precondition
base >= 2
(3)
Beispiel: number_of_digits (8, 2) = 4.
Programmieren Sie einen Kommandozeilenrechner, der eine ganze Zahl und
eine Basis ≥ 2 einnimmt und die Anzahl der Ziffern der Ganzzahl bzgl. der Basis
ausgibt!
Sprachen: Alle.
Aufgabe 3.7
Sprunganweisungen?
Nein danke!
Informatik 1 bis 3 handeln viel von strukturiertem Programmieren, wenig von Sprüngen. Trotzdem findet der pfiffige Student bald heraus, dass es in den C*-Sprachen die
Sprunganweisungen break, continue, manchmal sogar goto gibt, und er benutzt sie
sofort. Doch ist die bloße Existenz von Sprunganweisungen in einer Programmiersprache ein guter Grund, Spaghetticode zu produzieren, statt strukturierte Schleifen systematisch zu konstruieren? Ich erläutere die Semantik von break und continue hier, um
von ihrem Gebrauch abzuraten und in Informatik-3-Prüfungen Aufgaben stellen zu
können, bei denen unstrukturierte Schleifen in strukturierte umzuformen sind.
Semantik von break
und continue
while (...) {
...
if (...) break;
...
if (...) continue;
...
// continue jumps here
}
// break jumps here
1
for (...; ...; /* there */ ...) {
...
if (...) break;
...
if (...) continue;
...
// continue jumps here and then to there
}
// break jumps here
Ein Beispiel steht in http://de.wikipedia.org/wiki/Endrekursion (Zugriff 2010-11-25).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
Leitlinie 3.1
C*: Vermeide
Sprunganweisungen
3 – 17
Verwende goto und continue nie, break höchstens dann in einer Schleife, wenn sie
sonst eine zusätzliche Steuervariable oder Kopien von Anweisungen erfordert!
Benutze while und do-while für Bedingungsschleifen, for für Zählschleifen, break nur
bei unbedingten Schleifen! Konstruiere Bedingungsschleifen systematisch mit ihrer
Nachbedingung! Missbrauche Zählschleifen nicht durch Zuweisungen an Zählvariablen!
Formen Sie die folgenden C++-Programme schrittweise äquivalent so um, dass
(1)
(2)
(3)
(4)
Programm 3.31
C++: Beispiel von einer
IBM-Webseite
Programm 3.32
C++: Beispiel aus der
Prüfung Informatik 3
WS 2006/07
weder Sprunganweisungen noch missbrauchte Zählschleifen vorkommen,
nach jeder Bedingungsschleife ihre Nachbedingung als Zusicherung steht,
möglichst wenige Variablen möglichst lokal vereinbart sind,
die Funktion einen aussagekräftigen Namen hat!
for (i = 0; i < 5; i++) {
if (string[i] == '\0')
break;
length++;
}
void g (string s, string & t) {
int i, k;
bool b;
t = "";
for (i = 0; i < s.size(); i++) {
b = false;
if (i > 0) {
for (k = 0; k < t.size(); k++) {
if (s[i] == t[k]) {
b = true;
k = t.size();
}
}
}
if (b == false) {
t += s[i];
}
}
}
Sprachen: C*.
3.2
Größter gemeinsamer Teiler – objektorientiert
In 3.1 haben wir das Problem des größten gemeinsamen Teilers zweier Ganzzahlen
funktional gelöst und rekursive und iterative Implementationen der ggT-Funktion studiert. Zwischen funktionalem und objektorientiertem Programmieren liegen oft nur ein
paar Schritte.
Um die Verwandtschaft zwischen den Programmierparadigmen zu erkunden, lösen
wir nun das ggT-Problem in einem objektorientierten Programmierstil mit einfachen
Abstraktionen, und
aus software-didaktischen Gründen trennen wir die Aspekte Funktion und Ein-/
Ausgabe stärker als in 3.1 voneinander.
Die Darstellung beschränkt sich auf Eiffel und die C*-Sprachen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 18
3.2.1
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Entwurf
Der objektorientierte Ansatz packt beim funktionalen Ansatz auftretende Parameter
und Resultate in eine Klasse und erzeugt anstelle von Funktionsaufrufen neue Objekte.
Für die Lösung des ggT-Problems definieren wir eine Zahlenpaarklasse
PAIR_WITH_GCD mit
Abfragen p, q und gcd, das für ggT(p, q) steht, und
einer Aktion zum Setzen von p und q.
Objekte der Klasse sind so zu benutzen: Erst p und q setzen, dann gcd abfragen. Dem
liegt das Aktion-Abfrage-Schema zugrunde; es bedeutet, den Zustand eines Objekts
durch eine Aktion ändern und sich danach durch Abfragen informieren:
Aktion-AbfrageSchema
object.action
info := object.query
-- Change the state of object, but don’t get information from it.
-- Get state information from object, but don’t change its state.
Offenbar fordert das Aktion-Abfrage-Schema in Abfragen und Aktionen getrennte
Dienste der Klasse. Das Konzept der Trennung von Abfragen und Aktionen hat Bertrand Meyer als Teil der Vertragsmethode entwickelt und in Eiffel realisiert (wo Aktionen Kommandos (command) heißen). Im Dokument LS Leitlinien zum Softwareentwurf
erscheint es als Leitlinie LS.31. Das Aktion-Abfrage-Schema ist in jeder objektorientierten Programmiersprache anwendbar. Während gelegentlich nachteilig ist, dass sich
manche Anwendungsfälle nur umständlicher als mit nebeneffektbehafteten Funktionen
formulieren lassen, überwiegen die Vorteile:
☺ Die Aspekte Zustandsänderung und Zustandsabfrage sind sauber getrennt.
☺ Aktionen und Abfragen sind oft einfach. Im Beispiel ist die Aktion eine schlichte
Wann p, q setzen?
Wie gcd berechnen?
Bild 3.1
Kunde mit
Schnittstellenklasse
und Implementationsklassen
Setter-Prozedur, die Abfragen sind parameterlos.
Ein PAIR_WITH_GCD-Objekt soll mehrere ggT-Berechnungen nacheinander durchführen können. Dazu muss es jederzeit erlauben, p und q zu setzen, nicht nur bei der
Objektvereinbarung oder -erzeugung.
gcd berechnet den Wert ggT(p, q) nach rekursivem oder iterativem Muster. Die rekursive Variante von gcd benutzt nur lokale Zahlenpaarobjekte. Es bietet sich an, nach
Leitlinie LS.24 und Leitlinie LS.25 des Dokuments LS Leitlinien zum Softwareentwurf
von Implementationsvarianten zu abstrahieren und mit einer abstrakten Klasse zu
beginnen, die als Schnittstellenklasse die vollständige Schnittstelle der späteren Implementationsklassen festlegt, sodass das Beispiel auch die Konzepte Polymorphie und
dynamisches Binden demonstrieren kann.
GCD_CLIENT
root_action
make, main
PAIR_WITH_GCD abstract
query
action
p
q
gcd
abstract
set
PAIR_WITH_GCD_RR
PAIR_WITH_GCD_RI
query
query
gcd -- recursive
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
gcd -- iterative
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
Grundbegriffe der OO
3 – 19
Allgemein ist eine Klasse abstrakt, wenn sie mindestens einen abstrakten Dienst enthält. Ein Dienst ist abstrakt, wenn er nicht implementiert ist. Eine Größe ist polymorph, wenn sie sich dynamisch auf Objekte verschiedener Typen beziehen kann.
Wird von einer polymorphen Größe ein Dienst aufgerufen, so wird dynamisch gebunden, wenn die im dynamischen Typ der Größe vorliegende Implementation des Dienstes ausgeführt wird.
Den Ein-/Ausgabe-Teil, den ggT-Rechner, trennen wir in eine eigene Übersetzungseinheit GCD_CLIENT, die beide Funktionsvarianten benutzen kann. Ein drittes Kommandoargument nach den Ganzzahlen steuert, welche Variante den ggT-Wert berechnet:
Kommandozeile
gcd_client 12 34 r
gcd_client 12 34 Rotwein
-- Berechnung mit rekursiver Variante
gcd_client 12 34 i
gcd_client 12 34 blah
gcd_client 12 34
-- Berechnung mit iterativer Variante
3.2.2
Eiffel
3.2.2.1
Schnittstellenklasse
Programm 3.33
Eiffel: ggT,
objektorientiert abstrakt
note
description: "Abstraction for two integers and their greatest common divisor"
deferred class PAIR_WITH_GCD
feature
p,
q : INTEGER
set (new_p, new_q : INTEGER)
do
p := new_p
q := new_q
ensure
new_p_accepted: p = new_p
new_q_accepted: q = new_q
end -- set
gcd : INTEGER
-- Greatest common divisor of p and q.
deferred
end -- gcd
invariant
gcd_nonnegative:
gcd >= 0
gcd_bounded_by_p: p /= 0 implies gcd <= p.abs
gcd_bounded_by_q: q /= 0 implies gcd <= q.abs
end -- class PAIR_WITH_GCD
deferred
Eiffel nennt abstrakte Klassen und Merkmale aufgeschoben, da sie später – in Nachfolgerklassen – implementiert werden, und markiert beide mit dem Schlüsselwort deferred. Die Klasse PAIR_WITH_GCD ist zwar abstrakt, aber partiell implementiert: p und q
sind als Attribute, set als Setter-Prozedur fertig gestellt. Die Definition der Abfrage gcd
sieht zwar wie eine Attributdefinition aus, doch lässt ihre Markierung als aufgeschoben
offen, ob sie später als Attribut oder als parameterlose Funktion implementiert wird.
Die schwache Invariante für gcd zeigt exemplarisch, wie sich abstrakte Merkmale vertraglich spezifizieren lassen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 20
3.2.2.2
Programm 3.34
Eiffel: ggT, ooendrekursiv mit
Referenzsemantik
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Implementationsklassen
note
description: "Two integers and their greatest common divisor, referenced recursive"
class PAIR_WITH_GCD_RR
inherit
PAIR_WITH_GCD
create
set
feature
gcd : INTEGER
-- Implemented by Euclid's algorithm, tail-recursive.
local
next : PAIR_WITH_GCD_RR
do
if q = 0 then
Result := p.abs
else
create next.set (q, p \\ q)
Result := next.gcd
end
end -- gcd
end -- class PAIR_WITH_GCD_RR
Referenzsemantik
Objekterzeugung mit
expliziter Initialisierung
Objekterzeugung mit
Standardinitialisierung
Die objektorientiert-rekursive Implementationsvariante Programm 3.34 arbeitet wie in
Eiffel üblich mit Referenzobjekten; next ist eine lokale Referenz auf ein
PAIR_WITH_GCD_RR-Objekt, das mit der Erzeugungsanweisung
create next.set (q, p \\ q)
dynamisch angelegt wird. Da das geerbte set als Erzeugungsprozedur deklariert ist,
muss es bei der Objekterzeugung aufgerufen werden, um das Objekt garantiert korrekt
zu initialisieren. Wäre set keine Erzeugungsprozedur, so wären die zwei Anweisungen
create next
next.set (q, p \\ q)
hinzuschreiben: Das Objekt würde bei der Erzeugung mit Standardwerten (0) initialisiert und anschließend mit den gewünschten Werten versehen. In Programm 3.34 ist set
nur Erzeugungsprozedur zwecks kompakterer Objekterzeugung mit -initialisierung.
Eine Erzeugungsanweisung wie
Erzeugungsanweisung
create next
bewirkt, dass
Erzeugungsprozedur
ein Objekt des statischen Typs der Zielgröße next dynamisch erzeugt wird, d.h. Speicherplatz auf der Halde dafür reserviert wird,
die Attribute des Objekts mit Standardwerten wie False, 0, '', "", Void initialisiert
werden, und
die Zielgröße das erzeugte Objekt referenziert.
Die Standardinitialisierung würde zwar in Programm 3.34 genügen, aber nicht generell.
Erzeugungsprozeduren (creation procedure) dienen dem Zweck, Objekte ihrer
Klasse so zu initialisieren, dass ihr Anfangszustand konsistent ist, d.h. die Klasseninvariante erfüllt. Folglich dürfen sie die Gültigkeit der Invariante vor ihrem Aufruf nicht
voraussetzen. Eiffel-Klassen können beliebig viele Erzeugungsprozeduren mit frei
wählbaren Namen und beliebigen Signaturen haben. Hat eine Eiffel-Klasse Erzeu-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
3 – 21
gungsprozeduren, so muss bei jeder Objekterzeugung eine davon aufgerufen werden.
Erzeugungsprozeduren können nach der Objekterzeugung wie andere Prozeduren aufgerufen werden, z.B. zum Reinitialisieren eines Objekts.
Müllabfuhr
Endet ein Aufruf der gcd-Funktion, so endet auch die Existenz der lokalen Referenz
next, d.h. der von ihr belegte Speicherplatz im Laufzeitkeller wird freigegeben. Damit
wird das bisher von ihr referenzierte Objekt auf der Halde unerreichbar, belegt aber
weiter Speicherplatz. Wer gibt diesen Speicherplatz wann frei? Das besorgt die automatische Speicherbereinigung (garbage collector), die von Zeit zu Zeit hinter den
Kulissen Speicherplatz unerreichbarer Objekte einsammelt und freigibt. Den Programmierer braucht das nicht zu kümmern.
Effizienz
Freilich ist es noch aufwändiger, jedes auftretende Zahlenpaar in ein neues, dynamisch
auf der Halde angelegtes Objekt zu packen, als unverpackte Zahlenpaare im Laufzeitkeller zu stapeln wie beim funktional-rekursiven Ansatz!
Aufgabe 3.8
Eiffel: ggT, ooendrekursiv mit
Wertsemantik
Ändern Sie die ineffiziente Zahlenpaarklasse PAIR_WITH_GCD_RR von Programm 3.34
in objektorientiert-rekursive Varianten mit Wertsemantik, bei denen die Objekte dynamisch im Laufzeitkeller angelegt werden, sodass sich ihr Laufzeitaufwand dem der
funktional-rekursiven Variante nähert:
(1)
PAIR_WITH_GCD_VR arbeitet mit einzelnen Wertobjekten (expanded bei der Vereinbarung der Variablen next),
(2)
PAIR_WITH_GCD_XR ist eine expandierte Klasse (expanded bei der Definition
der Klasse),
und passen Sie die Kundenklasse GCD_CLIENT so an, dass sie auch die neuen Klassen
benutzt!
Beachten Sie: Ist next durch expanded (wo auch immer) eine Wertgröße, so werden – da
next lokal in der Funktion gcd vereinbart ist – die zu Aufrufen von gcd gehörenden
Objekte dynamisch im Laufzeitkeller angelegt. Solche Objekte initialisiert Eiffel implizit mit Standardwerten oder durch eine parameterlose Erzeugungsprozedur. Eiffel
erlaubt dabei wegen der Trennung von Vereinbarungen und Anweisungen in Vereinbarungen keine Aufrufe parametrisierter Erzeugungsprozeduren. Daher muss das parametrisierte set darauf verzichten, eine Erzeugungsprozedur zu sein. Das dynamische
Erzeugen des Objekts entfällt, es bleibt nur das Initialisieren.
Der funktional-iterative Ansatz wiederverwendet dieselben Variablen für die verschiedenen Zahlenwerte und spart so Funktionsaufrufe und Laufzeitkellerverwaltung. Entsprechend suchen wir eine objektorientiert-iterative Lösung, die man in der Praxis dort,
wo Effizienz wichtig ist, vorziehen kann.
Da die iterative Variante Programm 3.35 keine weiteren Zahlenpaarobjekte und damit
kein set benötigt, verzichten wir darauf, set als Erzeugungsprozedur zu deklarieren. Die
Schleife ist Programm 3.18 entnommen, ließe sich aber auch aus Programm 3.34 mit
dem Umformungsschema von S. 5 konstruieren. Damit ist der Funktionsteil der Problemlösung in zwei Varianten implementiert.
Programm 3.35
Eiffel: ggT, oo-iterativ
mit Referenzsemantik
note
description: "Two integers and their greatest common divisor, referenced iterative"
class PAIR_WITH_GCD_RI
inherit
PAIR_WITH_GCD
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 22
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
feature
gcd : INTEGER
-- Implemented by Euclid's algorithm, iterative.
local
s, t : INTEGER
do
from
Result := p.abs
s := q.abs
until s <= 0 loop
t := Result \\ s
Result := s
s := t
end
end -- gcd
end -- class PAIR_WITH_GCD_RI
3.2.2.3
Kundenklasse
Den Ein-/Ausgabe-Teil implementieren wir in Eiffel als Wurzelklasse GCD_CLIENT
(Programm 3.36). GCD_CLIENT benötigt von jeder Implementationsklasse des Funktionsteils ein Objekt, wofür wir die Variablen
pair_rr : PAIR_WITH_GCD_RR
pair_ri : PAIR_WITH_GCD_RI
lokal in der Wurzelprozedur make vereinbaren könnten. Wir wählen jedoch eine auf allgemeinere Situationen skalierende Entwurfsalternative.
Einmalfunktion
Programm 3.36
Eiffel: ggT-Rechner,
objektorientiert
Erstens sind pair_rr und pair_ri keine lokalen Variablen, sondern als Einmalfunktionen (once function) implementierte Abfragen, erkennbar daran, dass ihr Anweisungsteil mit dem Schlüsselwort once statt dem üblichen do beginnt. Dieser Anweisungsteil
wird nur einmal für alle Objekte (!) des Typs, zu dem er gehört, ausgeführt, und zwar
beim ersten Aufruf der Funktion. Alle Aufrufe liefern dasselbe Ergebnis. Eiffels Einmalfunktionen bilden ein Pendant zu den statischen Methoden der C*-Sprachen. Man
kann mit ihnen u.a. Module und Singletons nachbilden.
note
description: "Command-line calculator for greatest common divisor of two integers"
class GCD_CLIENT
create
make
feature {NONE}
pair_rr : PAIR_WITH_GCD
once
create {PAIR_WITH_GCD_RR} Result.set (0, 0)
ensure
pair_rr_exists: Result /= Void
end
pair_ri : PAIR_WITH_GCD
once
create {PAIR_WITH_GCD_RI} Result
ensure
pair_ri_exists: Result /= Void
end
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
3 – 23
feature
make (arguments : ARRAY [STRING])
-- Given two integer command arguments, print their greatest common divisor.
-- If 3rd argument starts with 'r' or 'R', then use recursive, else iterative variant.
local
pair : PAIR_WITH_GCD
do
if arguments = Void or else arguments.count < 3 or else
not arguments.item (1).is_integer or else
not arguments.item (2).is_integer
then
io.put_string ("Please call the gcd client with two integer arguments!")
else
if arguments.count > 3 and then
arguments.item (3).item (arguments.item (3).index_set.lower).as_lower = 'r'
then
pair := pair_rr
else
pair := pair_ri
end
pair.set (arguments.item (1).to_integer, arguments.item (2).to_integer)
io.put_integer (pair.gcd)
end
io.put_new_line
end -- make
end -- class GCD_CLIENT
In diesem Beispiel leisten die Einmalfunktionen dasselbe wie normale Funktionen, da
sie in make höchstens einmal aufgerufen werden. Gäbe es jedoch mehrere Aufrufe, so
würde eine normale Funktion bei jedem Aufruf ein neues Objekt erzeugen, während
die Einmalfunktion nur ein Objekt erzeugt und stets eine Referenz auf dieses liefert.
Polymorphe Größe
Zweitens sind pair_rr und pair_ri polymorph: Ihr statischer Typ ist PAIR_WITH_GCD,
ihr dynamischer Typ PAIR_WITH_GCD_RR bzw. PAIR_WITH_GCD_RI. Die Abfragen
verbergen ihren dynamischen Typ hinter ihrer Signatur. Der Kunde – hier make –
braucht nur ihren statischen Typ zu kennen, um sie benutzen zu können.
Drittens sehen wir eine Variante der Erzeugungsanweisung, mit der ein Objekt eines
explizit angegebenen Nachfolgertyps des Typs des Erzeugungsziels erzeugt wird:
Objekterzeugung mit
explizitem Typ
create {PAIR_WITH_GCD_RR} Result.set (0, 0)
Das Erzeugungsziel ist Result, sein statischer Typ ist PAIR_WITH_GCD. Der explizite
Erzeugungstyp ist PAIR_WITH_GCD_RR, ein Nachfolgertyp von PAIR_WITH_GCD. set
ist hier explizit aufzurufen, weil es Erzeugungsprozedur des Erzeugungstyps ist. Dagegen darf set in
create {PAIR_WITH_GCD_RI} Result
nicht aufgerufen werden, da es keine Erzeugungsprozedur des Erzeugungstyps
PAIR_WITH_GCD_RI ist. Beachten Sie die polymorphen Aufrufe
Polymorpher Aufruf
pair.set (...)
pair.gcd
in make!
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 24
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
3.2.3
C++
3.2.3.1
Schnittstellenklasse
Programm 2.7 und Programm 2.35 haben gezeigt, wie in C++ der Quelltext einer
Klasse auf eine Schnittstellen- und eine Implementationsdatei aufzuteilen ist. Die Aufteilung gilt auch für die partiell implementierte Schnittstellenklasse für Zahlenpaare.
Abstrakt
Virtuell
In C++ erkennt man abstrakte Klassen nicht an ihrem Kopf, sondern indem man die
Deklarationen der Elementfunktionen durchsucht, aber nicht nach einem Schlüsselwort
wie abstract, sondern vorne nach
virtual
und hinten nach
Pur
Programm 3.37
C++: ggT,
objektorientiert
abstrakt, Schnittstelle
= 0.
// Header file: PairWithGcd.hpp
#ifndef PAIR_WITH_GCD_HPP_
#define PAIR_WITH_GCD_HPP_
class PairWithGcd {
protected:
int p, q;
public:
int get_p () const {return p;}
int get_q () const {return q;}
virtual void set (int new_p, int new_q);
virtual int gcd () const = 0;
};
#endif // PAIR_WITH_GCD_HPP_
virtual ist ein Relikt von Simula 67 und bedeutet, dass die Funktion in Ableitungsklas-
sen überschreibbar (overridable, d.h. redefinierbar) ist und bei polymorphen Größen
(Zeigern, Referenzen) die zu ihrem dynamischen Typ gehörende Version der Funktion
dynamisch gebunden wird. „= 0“ wird pure gesprochen und bedeutet, dass die Funktion
abstrakt, also hier noch nicht implementiert ist. Stroustrup hat diese pseudoalgebraische Notation erfunden, um ein Schlüsselwort zu sparen.
Die Getter-Funktionen get_p, get_q definieren wir wie üblich als nicht virtuelle, konstante Inline-Funktionen in der Klasse. Setter-Funktionen werden auch oft als nicht virtuell definiert. Aber wer kann hellsehen? Wir beachten die Leitlinie 2.13 S. 2-33.
Programm 3.38
C++: ggT,
objektorientiert
abstrakt, partielle
Implementation
3.2.3.2
// Implementation file: PairWithGcd.cpp
#include "PairWithGcd.hpp"
void PairWithGcd::set (int new_p, int new_q) {
p = new_p;
q = new_q;
}
Implementationsklassen
Die C++-Syntax für Vererbung – Ableitung (derivation) genannt –, ist ein „:“:
class PairWithGcdR : public PairWithGcd
Das public vor dem Basisklassennamen bedeutet, dass die geerbten Elemente ihren
Exportstatus von der Basisklasse übernehmen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
Programm 3.39
C++: ggT, ooendrekursiv,
Schnittstelle
3 – 25
// Header file: PairWithGcdR.hpp
#ifndef PAIR_WITH_GCD_R_HPP_
#define PAIR_WITH_GCD_R_HPP_
#include "PairWithGcd.hpp"
class PairWithGcdR : public PairWithGcd {
public:
PairWithGcdR (int new_p = 0, int new_q = 0);
int gcd () const;
};
#endif // PAIR_WITH_GCD_R_HPP_
Konstruktor
Die rekursive Implementationsvariante Programm 3.39 benötigt lokale Objekte, die bei
ihrer Vereinbarung oder Erzeugung Werte für p und q erhalten sollen. Die Setter-Funktion set genügt dafür nicht, wir müssen zusätzlich einen Konstruktor definieren. In den
C*-Sprachen entsprechen Konstruktoren Eiffels Erzeugungsprozeduren. Konstruktoren (constructor)
dienen dem Zweck, Objekte ihrer Klasse so zu initialisieren, dass ihr Anfangszustand konsistent ist,
haben keine Namen, heißen aber praktisch stets wie ihre Klasse,
treten in einer Klasse beliebig zahlreich mit unterschiedlichen Signaturen auf,
haben keinen Ergebnistyp (auch nicht void),
müssen und dürfen nur bei statischen Objektvereinbarungen und dynamischen
Objekterzeugungen aufgerufen werden (dürfen also danach nicht wie normale
Funktionen benutzt werden, auch nicht zum Reinitialisieren von Objekten),
dürfen Elementfunktionen aufrufen.
Die Konstruktordeklaration
Standardargument
PairWithGcdR (int new_p = 0, int new_q = 0);
versieht die formalen Parameter new_p, new_q durch „= 0“ mit Standardargumenten.
Fehlen bei einem Konstruktoraufruf aktuelle Parameter, so werden dafür automatisch
die Standardargumente eingesetzt. Damit fungiert dieser Konstruktor auch als parameterloser Standardkonstruktor.
Die Funktion gcd wird für ihre Definition im Implementationsteil noch einmal deklariert. Sie bleibt nach der Regel „einmal virtuell, immer virtuell“ auch ohne Markierung
virtual virtuell.
Programm 3.40
C++: ggT, ooendrekursiv,
Implementation mit
Wertsemantik
// Implementation file: PairWithGcdR.cpp
#include "PairWithGcdR.hpp"
#include <cstdlib>
PairWithGcdR::PairWithGcdR (int new_p, int new_q) {
set(new_p, new_q);
}
int PairWithGcdR::gcd () const {
if (q == 0) {
return abs(p);
} else {
PairWithGcdR next = PairWithGcdR(q, p % q);
return next.gcd();
}
}
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 26
Initialisierung
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Der Konstruktor ruft die schon in der Basisklasse implementierte set-Funktion auf.
Dass ein Konstruktor andere Elementfunktionen – auch virtuelle! – aufrufen darf, ist
nicht unproblematisch, denn diese könnten voraussetzen, dass das Objekt schon initialisiert und die Invariante erfüllt ist. Da C++ keine Unterstützung bietet, um Invarianten
zu formulieren und zu prüfen, muss der aufmerksame Programmierer selbst mögliche
Henne-Ei-Probleme beim Initialisieren vermeiden. Im Beispiel tritt kein Problem auf.
Eine Alternative zum Setter-Aufruf bietet eine Initialisierungsliste, die vor dem dann
leeren Anweisungsblock steht und die Standardinitialisierung ersetzt [C++98] 12.6.2
Initializing bases and members S. 197–200:
PairWithGcdR::PairWithGcdR (int new_p, int new_q) : p (new_p), q (new_q) {}
Diese syntaktische C++-Spezialität ist effizienter als die Initialisierung im Anweisungsblock, repliziert aber Initialisierungscode im Konstruktor und im Setter.
Wertsemantik
Programm 3.40 arbeitet mit Wertsemantik; sie ist in C++ einfach zu notieren: Die PairWithGcd-Objekte werden durch die Deklaration mit Initialisierungsteil
PairWithGcdR next = PairWithGcdR(q, p % q);
statisch lokal vereinbart, dynamisch im Laufzeitkeller angelegt und durch einen Konstruktoraufruf initialisiert. Eine abkürzende Notation dafür ist
PairWithGcdR next (q, p % q);
Zeigersemantik
Für die Variante Programm 3.41 mit Zeigersemantik ist gegenüber Programm 3.40 im
Rumpf der gcd-Funktion zu ändern:
next durch „*“ als Zeiger vereinbaren,
das Objekt durch new dynamisch auf der Halde erzeugen,
next mit „->“ explizit dereferenzieren und selektieren.
Programm 3.41
C++: ggT, ooendrekursiv,
Implementation mit
Zeigersemantik
// Implementation file: PairWithGcdR.cpp
...
#include <cassert>
int PairWithGcdR::gcd () const {
if (q == 0) {
return abs(p);
} else {
PairWithGcdR * next = new PairWithGcdR(q, p % q);
assert(next); // new successful.
int result = next->gcd();
delete next;
return result;
}
}
...
Da C++ die Halde nicht automatisch bereinigt, ist das Objekt nach dem Benutzen
durch delete explizit zu löschen. Da die Löschanweisung vor der return-Anweisung stehen muss, ist der Ergebniswert zwischenzuspeichern.
Speicherüberlauf
Das assert-Vorübersetzermakro prüft, ob next ungleich null ist, d.h. ob new erfolgreich
ein Objekt erzeugt hat. Die Objekterzeugung könnte wegen Speichermangel fehlschlagen, denn aus der manuellen Speicherverwaltung in C++ folgt das Problem des Speicherüberlaufs (memory leak) durch nicht gelöschte, unerreichbare Objekte. In C++ löst
ein erfolgloses new nicht etwa eine Ausnahme aus, sondern liefert nur null zurück.
Da das iterative Programm 3.42 keine Objekte benötigt, verzichten wir auf einen Konstruktor. Die Schleife in Programm 3.43 ist Programm 3.22 entnommen, ließe sich aber
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
3 – 27
auch aus Programm 3.41 mit dem Umformungsschema von S. 5 konstruieren. Damit ist
der Funktionsteil der Problemlösung in zwei Varianten implementiert.
Programm 3.42
C++: ggT, oo-iterativ,
Schnittstelle
// Header file: PairWithGcdI.hpp
#ifndef PAIR_WITH_GCD_I_HPP_
#define PAIR_WITH_GCD_I_HPP_
#include "PairWithGcd.hpp"
class PairWithGcdI : public PairWithGcd {
public:
int gcd () const;
};
#endif // PAIR_WITH_GCD_I_HPP_
Programm 3.43
C++: ggT, oo-iterativ,
Implementation
// Implementation file: PairWithGcdI.cpp
#include "PairWithGcdI.hpp"
#include <cstdlib>
int PairWithGcdI::gcd () const {
int r = abs(p),
s = abs(q),
t;
while (s > 0) {
t = r % s;
r = s;
s = t;
}
return r;
}
3.2.3.3
Kundenfunktion
Die C++-Variante des ggT-Rechners, im Entwurf als GCD_CLIENT bezeichnet, realisieren wir als eine Implementationsdatei mit einer main-Funktion. Für Objekte der Implementationsvarianten der Zahlenklasse könnten wir lokale Variablen in main vereinbaren, bevorzugen aber eine der Eiffel-Variante entsprechende skalierende Alternative.
In Programm 3.44 sind pair_r und pair_i durch
Konstanter Zeiger
static PairWithGcd * const pair_r = new PairWithGcdR();
static PairWithGcd * const pair_i = new PairWithGcdI();
als konstante Zeigervariablen mit obligatorischen Initialisierungsteilen definiert, deren
Sichtbarkeit wegen static auf die Datei beschränkt ist, die die Definition enthält. Die
Initialisierungen werden einmal beim Start des Programms vor dem Aufruf von main
ausgeführt, dabei werden die Objekte dynamisch erzeugt, auf die die Zeiger dann
unveränderlich zeigen. Die Objekte existieren bis zum Ende des Programmablaufs.
Polymorpher Zeiger
pair_r und pair_i sind polymorph: Ihr statischer Zeigerbasistyp ist PairWithGcd, die
dynamischen Objekttypen sind durch die Konstruktoraufrufe festgelegt als
PairWithGcdR bzw. PairWithGcdI. Die Zeiger verbergen ihren dynamischen Typ vor
main, das nur ihren statischen Typ kennt, um sie benutzen zu können. Bei
new PairWithGcdR()
wird der explizit programmierte Konstruktor mit Standardargumenten aufgerufen, bei
new PairWithGcdI()
der implizit definierte Standardkonstruktor. Polymorphe Aufrufe in main sind:
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 28
Polymorpher Aufruf
Programm 3.44
C++: ggT-Rechner,
objektorientiert
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
pair->set(...)
pair->gcd()
// Implementation file: GcdClient.cpp
#include "PairWithGcdR.hpp"
#include "PairWithGcdI.hpp"
#include <cstdlib>
#include <iostream>
static PairWithGcd * const pair_r = new PairWithGcdR();
static PairWithGcd * const pair_i = new PairWithGcdI();
int main (int argc, char * argv[]) {
if (argc < 3) {
std::cout << "Please call the gcd client with two integer arguments!";
} else {
PairWithGcd * pair;
if (argc > 3 && tolower(*(argv[3])) == 'r') {
pair = pair_r;
} else {
pair = pair_i;
}
pair->set(std::atoi(argv[1]), std::atoi(argv[2]));
std::cout << pair->gcd();
}
std::cout << std::endl;
return 0;
}
Programm 3.44 löscht die Objekte an pair_r und pair_i nach ihrer Benutzung nicht.
Diese Ordnungswidrigkeit ist hier tolerierbar, weil der Programmablauf gleich endet
und das Betriebssystem den vermüllten Speicher zurück erhält.
3.2.4
Java
extends
Da Java bei Klassen nur Referenzsemantik kennt, folgen weniger Varianten als bei Eiffel und C++. Zudem sind die meisten verwendeten Sprachelemente bekannt. Vererbung heißt in Java Erweiterung (extension) und wird mit dem Schlüsselwort extends
ausgedrückt. Javas Exportpolitik müssen wir jedoch näher betrachten. Java bietet vier
Schutzzustände für Felder und Methoden von Klassen:
Schutz und
Zugriffsrechte
Der Modifikator public exportiert das markierte Merkmal an alle Klassen. public
bei Datenfeldern widerspricht dem Konzept der Datenabstraktion und ist daher zu
vermeiden (s. Leitlinie 2.11 S. 2-31).
Der Modifikator protected exportiert das Merkmal an alle Klassen im selben Paket
und an alle Erweiterungsklassen auch in anderen Paketen. Damit widerspricht auch
protected bei Feldern dem Konzept der Datenabstraktion. Eine Klasse kann ihre
Invariante nicht allein garantieren, sondern ein Paket ist für die Korrektheit seiner
Klassen verantwortlich. Da sich Pakete nicht abschließen, sondern leicht um weitere
Klassen erweitern lassen, ist protected problematisch.
Ohne Schutzmodifikator wird das Merkmal an alle Klassen im selben Paket exportiert. Bei Feldern widerspricht auch dies dem Konzept der Datenabstraktion und ist
so bedenklich wie public und protected.
Der Modifikator private exportiert das Merkmal an keine andere Klasse, nicht einmal an Erweiterungsklassen. Solch strenger Schutz ist selten nützlich.
Objektorientierte Entwürfe verlangen oft, Zugriffsrechte auf Merkmale an Erweiterungsklassen, aber nicht an Kundenklassen zu vergeben. Was sagt Java dazu?
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
Leitlinie 3.2
Java: Bändige
protected stark
3 – 29
Soll eine Klasse Merkmale nur an ihre Erweiterungen, aber nicht an Kunden exportieren, so definiere ein Paket, das nur diese Klasse enthält, und markiere die Merkmale, die an Erweiterungen zu exportieren sind, mit protected.
Dass Java (anders als die anderen vier Auswahlsprachen) nur diese Option bietet,
befriedigt kaum, da es aufwändig ist, zur Proliferation kleiner Pakete führt und der Idee
des Pakets als Ansammlung von Klassen widerspricht. Deshalb schlage ich als Kompromiss vor:
Leitlinie 3.3
Java: Bändige
protected
Soll eine Klasse Merkmale nur an ihre Erweiterungen, nicht an Kunden exportieren,
so definiere ein Paket, das nur diese Klasse und Erweiterungen von ihr enthält, und
markiere die Merkmale, die an Erweiterungen zu exportieren sind, mit protected.
Eine Erweiterungsklasse kann dann zwar auch in einer Kundenrolle auf ein protected
Merkmal zugreifen, doch toleriere ich diese Verletzung der Datenabstraktion gegen
weniger Aufwand. Den Entwurf von Bild 3.1 setze ich demnach so um:
Die Schnittstellenklasse PairWithGcd liegt mit ihren Implementationsklassen
PairWithGcdR und PairWithGcdI im Paket Pairs.
Die Kundenklasse GcdClient liegt außerhalb des Pakets Pairs.
3.2.4.1
Programm 3.45
Java: ggT,
objektorientiert abstrakt
Schnittstellenklasse
package Pairs; // Only to be used for extensions of PairWithGcd.
public abstract class PairWithGcd {
protected int p, q;
public int p () {return p;}
public int q () {return q;}
public void set (int p, int q) {
this.p = p;
this.q = q;
}
public abstract int gcd ();
}
Abstrakt
Java markiert abstrakte Klassen und Methoden mit dem Modifikator abstract. Alle
Methoden werden dynamisch gebunden und sind in Erweiterungsklassen redefinierbar,
sofern sie zugreifbar und nicht als final markiert sind.
3.2.4.2
Implementationsklassen
Programm 3.46
Java: ggT, ooendrekursiv
package Pairs; // Only to be used for extensions of PairWithGcd.
public class PairWithGcdR extends PairWithGcd {
public PairWithGcdR (int p, int q) {
set(p, q);
}
public int gcd () {
if (q == 0) {
return Math.abs(p);
} else {
PairWithGcdR next = new PairWithGcdR(q, p % q);
return next.gcd();
}
}
}
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 30
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Weder das rekursive Programm 3.46 noch das iterative Programm 3.47 bieten Neues.
Zu Konstruktoren in Java sagt der C++-Abschnitt 3.3.8 das hier Wesentliche. Durch
die Deklaration mit Initialisierungsteil in Programm 3.46
Referenzsemantik
PairWithGcdR next = new PairWithGcdR(q, p % q);
werden die PairWithGcdR-Objekte dynamisch auf der Halde angelegt und durch einen
Konstruktoraufruf initialisiert. Nachdem sie unerreichbar geworden sind, sorgt eine
automatische Speicherbereinigung für die Freigabe ihres Speicherplatzes.
Programm 3.47
Java: ggT, oo-iterativ
package Pairs; // Only to be used for extensions of PairWithGcd.
public class PairWithGcdI extends PairWithGcd {
public PairWithGcdI (int p, int q) {
set(p, q);
}
public int gcd () {
int r = Math.abs(p),
s = Math.abs(q),
t;
while (s > 0) {
t = r % s;
r = s;
s = t;
}
return r;
}
}
3.2.4.3
Programm 3.48
Java: ggT-Rechner,
objektorientiert
Kundenklasse
public class GcdClient {
protected static final PairWithGcd pairR = new PairWithGcdR(0, 0);
protected static final PairWithGcd pairI = new PairWithGcdI(0, 0);
protected static final String HINT =
"Please call the gcd client with two integer arguments! ";
public static void main (String[] args) {
if (args.length < 2) {
System.out.println("Missing Argument. " + HINT);
} else {
PairWithGcd pair;
if (args.length > 2 &&
Character.toLowerCase(args[2].charAt(0)) == 'r') {
pair = pairR;
} else {
pair = pairI;
}
try {
pair.set
(Integer.parseInt(args[0]), Integer.parseInt(args[1]));
System.out.println(pair.gcd());
} catch (Exception e) {
System.out.println("Wrong Argument. " + HINT + e.toString());
}
}
}
}
Die Java-Variante des ggT-Rechners realisieren wir wie in Eiffel als eine Klasse GcdClient mit einer main-Funktion. Für Objekte der Implementationsvarianten der Zah-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
3 – 31
lenklasse könnten wir lokale Variablen in main vereinbaren, bevorzugen aber eine der
Eiffel-Variante entsprechende skalierende Alternative.
pairR
Konstante Referenz
und pairI sind durch
protected static final PairWithGcd pairR = new PairWithGcdR(0, 0);
protected static final PairWithGcd pairI = new PairWithGcdI(0, 0);
als konstante Referenzen definiert. Die obligatorischen Initialisierungsteile werden einmal beim Programmstart vor dem Aufruf von main ausgeführt, dabei werden die
Objekte dynamisch erzeugt, auf die die Referenzen dann unveränderlich zeigen.
Polymorphe Referenz
und pairI sind polymorph: Ihr statischer Typ ist PairWithGcd, die dynamischen Typen sind durch die Konstruktoraufrufe festgelegt als PairWithGcdR bzw.
PairWithGcdI. Auch die Referenzen verbergen ihre dynamischen Typen vor ihrem
Kunden main, das nur ihren statischen Typ kennt, um sie benutzen zu können. Polymorphe Aufrufe in main sind:
pairR
Polymorpher Aufruf
3.2.5
pair.set(...)
pair.gcd()
C#
Da C# zwar Wert- und Referenzsemantik kennt, aber ohne gemeinsame Abstraktionen,
folgen mehr Varianten als bei Java mit mehr Schreibaufwand, die teilweise Ihnen als
Übung überlassen sind. Da C# (wie C++, aber anders als Java) mit protected markierte Merkmale nur an Erweiterungsklassen exportiert, lassen sich die PairWithGcdKlassen ohne eigenen Namensraum (das C#-Pendant zum Java-Paket) besser als in
Java mit eigenem Paket schützen.
3.2.5.1
Programm 3.49
C#: ggT,
objektorientiert
abstrakt, Referenztyp
Schnittstellenklassen
public abstract class PairWithGcdR {
protected int p, q;
public int P {get {return p;}}
public int Q {get {return q;}}
public virtual void Set (int p, int q) {
this.p = p;
this.q = q;
}
public abstract int Gcd ();
}
Abstrakt
Virtuell
C# markiert abstrakte Klassen und Methoden wie Java mit dem Modifikator abstract.
Aber nur mit virtual oder abstract markierte Methoden werden dynamisch gebunden und sind in Ableitungsklassen redefinierbar. Abstrakte Klassen sind nur als Basisklassen anderer Klassen mit Referenzsemantik nutzbar.
Schnittstelle
Für Wertsemantik bietet C# Strukturtypen (struct). Gemeinsame Schnittstellenteile
von struct-Typen lassen sich in Interfaces (interface) abstrahieren. Von der abstrakten Klasse Programm 3.49 erhalten wir das Interface-Programm 3.50 so:
durch interface ersetzen,
alle Implementationsteile entfernen, im Einzelnen alle
mit protected oder private markierten Elemente,
Datenelemente,
Anweisungsteile von Properties und Methoden,
alle Modifikatoren public, virtual, abstract entfernen.
abstract class
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 32
Programm 3.50
C#: ggT,
objektorientiert
abstrakt, Werttyp
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
public interface PairWithGcdV {
int P {get;}
int Q {get;}
void Set (int p, int q);
int Gcd ();
}
3.2.5.2
Programm 3.51
C#: ggT, ooendrekursiv,
Referenztyp
Implementationsklassen
public class PairWithGcdRR : PairWithGcdR {
public PairWithGcdRR (int p, int q) {
Set(p, q);
}
public override int Gcd () {
if (q == 0) {
return System.Math.Abs(p);
} else {
PairWithGcdR next = new PairWithGcdRR(q, p % q);
return next.Gcd();
}
}
}
:
override
Bei Klassen – den Referenztypen – spricht C# von Vererbung und Ableitung und
drückt die Beziehung durch „:“ aus. Das rekursive Programm 3.51 bietet kaum Neues:
Redefinitionen von Methoden müssen mit override markiert werden. Sonst gelten die
Bemerkungen zur Java-Variante auf S. 29f.
Aufgabe 3.9
C#: ggT, oo-iterativ,
Referenztyp
Ändern Sie die ineffiziente Zahlenpaarklasse PairWithGcdRR von Programm 3.51 in
eine objektorientiert-iterative Variante PairWithGcdRI mit Referenzsemantik!
Programm 3.52
C#: ggT, oo-iterativ,
Werttyp
public struct PairWithGcdVI : PairWithGcdV {
private int p, q;
public int P {get {return p;}}
public int Q {get {return q;}}
public void Set (int p, int q) {
this.p = p;
this.q = q;
}
public int Gcd () {
int r = System.Math.Abs(p),
s = System.Math.Abs(q),
t;
while (s > 0) {
t = r % s;
r = s;
s = t;
}
return r;
}
}
Wertsemantik
Die Wertsemantik-Variante Programm 3.52 zeigt zur Abwechslung die iterative Implementation. Strukturtypen können zwar nicht erben, aber wenigstens Interfaces implementieren. Auch diese Beziehung drückt C# durch „:“ aus. Die aus dem Interface-Programm 3.50 entfernten Implementationsteile erscheinen wieder im implementierenden
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.2 Größter gemeinsamer Teiler – objektorientiert
3 – 33
Strukturtyp. Allerdings darf ein struct-Konstruktor keine Methode aufrufen. Deshalb
verzichten wir auf einen programmiererdefinierten Konstruktor und begnügen uns mit
dem Standardkonstruktor. Zu erwähnen bleibt, dass Datenelemente nicht protected
sein dürfen (da Stukturtypen erbenlos sind), folglich als private zu markieren sind.
Aufgabe 3.10
C#: ggT, oo-rr, Werttyp
Ändern Sie den Zahlenpaartyp PairWithGcdVI von Programm 3.52 in eine objektorientiert-endrekursive Variante PairWithGcdVR mit Wertsemantik!
3.2.5.3
Kundenklasse
Die C#-Variante des ggT-Rechners realisieren wir wie in Eiffel und Java als eine Klasse
GcdClient mit einer Main-Funktion in der skalierenden Variante. Da aber Main trotz
der vier Implementationsvarianten kompakt bleiben soll, verzichten wir auf das dritte
Kommandoargument zur Variantenauswahl und lassen Main die Berechnung durch alle
vier Varianten zugleich ausführen.
Programm 3.53
C#: ggT-Rechner,
objektorientiert
public class GcdClient {
protected
protected
protected
protected
static
static
static
static
PairWithGcdR
PairWithGcdR
PairWithGcdV
PairWithGcdV
pairRR
pairRI
pairVR
pairVI
=
=
=
=
new
new
new
new
PairWithGcdRR(0, 0);
PairWithGcdRI(0, 0);
PairWithGcdVR();
PairWithGcdVI();
protected const string Hint =
"Please call the gcd client with two integer arguments! ";
public static void Write (string name, int p, int q, int gcd) {
System.Console.WriteLine
("Gcd" + name + " (" + p + ", " + q + ") = " + gcd);
}
public static void Main (string[] args) {
if (args.Length < 2) {
System.Console.WriteLine("Missing Argument. " + Hint);
} else {
try {
int p = System.Convert.ToInt32(args[0]);
int q = System.Convert.ToInt32(args[1]);
pairRR.Set(p, q);
pairRI.Set(p, q);
pairVR.Set(p, q);
pairVI.Set(p, q);
Write("RR", p, q, pairRR.Gcd());
Write("RI", p, q, pairRI.Gcd());
Write("VR", p, q, pairVR.Gcd());
Write("VI", p, q, pairVI.Gcd());
} catch (System.Exception e) {
System.Console.WriteLine
("Wrong Argument. " + Hint + e.Message);
}
}
}
}
pairRR, pairRI, pairVR
Statische Referenz
protected
protected
protected
protected
static
static
static
static
und pairVI sind durch
PairWithGcdR
PairWithGcdR
PairWithGcdV
PairWithGcdV
pairRR
pairRI
pairVR
pairVI
=
=
=
=
new
new
new
new
PairWithGcdRR(0, 0);
PairWithGcdRI(0, 0);
PairWithGcdVR();
PairWithGcdVI();
als statische Referenzen definiert. Die Initialisierungsteile werden einmal beim Programmstart vor dem Aufruf von Main ausgeführt, dabei werden für
pairRR, pairRI
27.9.12
Objekte dynamisch auf der Halde,
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 34
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
pairVR, pairVI
Polymorphe Referenz
3.2.6
Wertvariablen direkt bei der Klasse GcdClient
angelegt, auf die die Referenzen dann zeigen. Wie in der Java-Variante sind die Referenzen polymorph: Ihre statischen Typen sind die abstrakte Klasse PairWithGcdR bzw.
das Interface PairWithGcdV, die dynamischen Typen sind durch die Konstruktoraufrufe
festgelegt. Bei pairRR, pairRI werden programmiererdefinierte Konstruktoren aufgerufen, bei pairVR, pairVI Standardkonstruktoren. Beachten Sie die polymorphen Aufrufe in Main!
Fazit
Der Entwurf objektorientierter Lösungen des ggT-Problems von Bild 3.1 lässt sich in
allen Auswahlsprachen ähnlich realisieren. Alle Sprachen kennen abstrakte Klassen.
Redefinierbarkeit, Polymorphie und dynamisches Binden
sind Standard (Component Pascal, Eiffel, Java),
erfordern speziell markierte Methoden (C++, C#).
Der Export von Merkmalen nur an Erweiterungsklassen ist
aufwändig durch disziplinierte Zuordnung von Klassen zu Modulen bzw. Paketen
(Component Pascal, Java),
leicht durch ein spezielles Sprachkonstrukt (Eiffel, C++, C#)
zu erreichen. Daten werden
teilweise (Component Pascal),
immer (Eiffel, C*)
mit Standardwerten initialisiert. Die Initialisierung von Objekten
obliegt der Verantwortung des Programmierers (Component Pascal),
wird durch die Sprache erzwungen, und zwar durch
Erzeugungsprozeduren (Eiffel),
Konstruktoren (C*).
Als Speicherarten gibt es
einen globalen Datenspeicher, und zwar in
statischer Form für statisch gebundene Programme (C++),
dynamischer Form für dynamisch geladene Module bzw. Klassen (Component
Pascal, Java, C#),
einen Laufzeitkeller und eine Halde (Component Pascal, Eiffel, C*).
Die Freigabe nicht mehr benötigten Haldenspeichers erfolgt
manuell durch den Programmierer (C++),
automatisch durch eine Speicherbereinigung (garbage collector) (Component Pascal, Eiffel, Java, C#).
3.2.7
Aufgaben
Aufgabe 3.11
Ablaufverfolgung einer
ggT-Berechnung
Verfolgen Sie den Ablauf der Berechnung des größten gemeinsamen Teilers eines
Paars mindestens zweistelliger Zahlen unter der
(1)
(2)
(3)
(4)
(5)
funktional-iterativen,
funktional-rekursiven,
objektorientiert-iterativen,
objektorientiert-rekursiven Wertsemantik-,
objektorientiert-rekursiven Referenz- oder Zeigersemantik-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3 – 35
Implementation des euklidischen Algorithmus! Zeichnen Sie dazu die Objekte im
Laufzeitkeller und auf der Halde auf! Abstrahieren Sie dabei von speziellen Sprachen!
Sprachen: Alle, sofern sie die Implementation erlauben.
Aufgabe 3.12
ggT- und kgV-Kunde
(1)
(2)
Testen Sie die Zahlenpaarklassen und den ggT-Kunden nach 3.2!
Ergänzen Sie
die Zahlenpaarklassen um eine Funktion für das kleinste gemeinsame Vielfache (sie lässt sich als Schablonenmethode implementieren – wo?),
den ggT-Kunden um die Ausgabe des kleinsten gemeinsamen Vielfachen der
eingegebenen Zahlen!
Sprachen: Alle.
Aufgabe 3.13
Zahlenpaarklassen,
effizienter
Bei den Zahlenpaarklassen von 3.2 gilt: Auch wenn p, q ihre Werte behalten, berechnet
die gcd-Funktion bei jedem Aufruf denselben Wert neu. Wenn p, q selten gesetzt und
gcd oft abgefragt wird, ist dies ineffizient. Suchen Sie eine effizientere Lösung!
Sprachen: Alle.
Aufgabe 3.14
Stellenzahl,
objektorientiert
Entwickeln Sie für die Probleme aus Aufgabe 3.6 objektorientierte Lösungen!
Aufgabe 3.15
Ziffernquadratsummenfolge
Entwerfen Sie zu dem Ein-/Ausgabe-Problem
Sprachen: Alle.
Eingabe:
Zwei positive ganze Zahlen m und n.
Ausgabe:
Eine Folge von n positiven ganzen Zahlen, von denen die erste gleich m ist
und jede folgende gleich der Summe der Quadrate der Dezimalziffern der
Vorgängerzahl.
funktionale und objektorientierte Lösungen mit rekursiven und iterativen Algorithmen!
Programmieren Sie Kommandozeilenrechner dazu, testen Sie das Programm und verbalisieren Sie das Geheimnis dieser Zahlenfolgen!
Sprachen: Alle.
3.3
Abstraktion mit Konzeptklassen und Schablonenmethoden
In 3.2 haben wir objektorientierte Lösungen des ggT-Problems studiert. Nun
extrahieren wir daraus abstraktere Lösungen zu abstrakteren Problemen,
die wir mit Konzeptklassen beschreiben und
partiell als Schablonenmethoden (template method), einem objektorientierten Entwurfsmuster (design pattern) aus dem Katalog von Gamma u.a., implementieren
[GHJV04].
Das Schema zur Umformung endrekursiver in iterative Algorithmen von S. 5 wenden
wir noch einmal auf höherem Abstraktionsniveau an.
Bild 3.2
Abstraktes Problem mit
abstrakter Lösung
PROBLEM_SOLVER
Anfangswerte
einstellen
Lösungswerte
abfragen
PROBLEM
SOLUTION
Lösungswerte
einstellen
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 36
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Von den Konzeptklassen in Bild 3.2 fordern wir:
Anforderungen
(1)
(2)
(3)
(4)
PROBLEM_SOLVER kann ein PROBLEM-Objekt erzeugen und bei diesem beliebig
oft Anfangswerte einstellen.
PROBLEM hat ein SOLUTION-Objekt, das nur PROBLEM selbst erzeugen und bei
dem nur PROBLEM die Lösungswerte einstellen kann.
PROBLEM erzeugt sein SOLUTION-Objekt nur einmal und wiederverwendet es
für Lösungen zu nacheinander eingestellten Anfangswerten.
PROBLEM_SOLVER kann die Lösungswerte beim SOLUTION-Objekt seines PROBLEM-Objekts abfragen, aber nicht verändern.
PROBLEM_SOLVER arbeitet
Programm 3.54
Pseudocode:
Problemlöser
3.3.1
nach dem Aktion-Abfrage-Schema etwa so:
class PROBLEM_SOLVER
root_action
make, main is
do
problem : PROBLEM
problem.set (start_values)
solution_values := problem.solution.values
end
end
Teile und herrsche – funktional
Wir konzentrieren uns zunächst auf den PROBLEM-Teil und suchen eine funktionale
Lösung. Zum ggT-Problem stellt Programm 3.1 eine endrekursive Lösung in funktionalem Pseudocode dar. Vom konkreten Problem abstrahierend betrachten wir eine allgemeine Kategorie von Problemen, für die eine ähnlich einfache endrekursive Lösung
existiert. Das Lösungsverfahren ist ein Beispiel für Verfahren, die dem Teile-und-herrsche-Prinzip (divide and conquer) folgen, das sich verbal so beschreiben lässt:
Teile-und-herrscheVerfahren
Ist das Problem direkt lösbar, nimm die direkte Lösung als Lösung, sonst zerlege
das Problem in Teilprobleme derselben Art und wende darauf das Lösungsverfahren
rekursiv an.
Genauer erfüllen die Probleme der betrachteten Kategorie DIVISIBLE_PROBLEM diese
Bedingungen:
Eigenschaften teilbarer
Probleme
(1)
(2)
(3)
(4)
Das Problem kann so einfach sein, dass es direkt lösbar ist.
Ein nicht direkt lösbares Problem lässt sich auf ein einfacheres Teilproblem derselben Art reduzieren.
Die Reduktion führt in endlich vielen Schritten zu einem direkt lösbaren Teilproblem.
Die Lösung eines Teilproblems ist gleichzeitig Lösung des größeren Problems,
aus dessen Reduktion es entstanden ist.
Programm 3.55 formuliert das allgemeine Lösungsverfahren in eiffelischem funktionalem Pseudocode.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
Programm 3.55
Funktionaler
Pseudocode:
Endrekursiv gelöstes
Problem
3 – 37
directly_solvable (problem : DIVISIBLE_PROBLEM) : BOOLEAN is abstract
direct_solution (problem : DIVISIBLE_PROBLEM) : SOLUTION is
precondition
directly_solvable (problem)
abstract
part (problem : DIVISIBLE_PROBLEM) : DIVISIBLE_PROBLEM is
precondition
not directly_solvable (problem)
abstract
solution (problem : DIVISIBLE_PROBLEM) : SOLUTION is
if directly_solvable (problem) then
direct_solution (problem)
else
solution (part (problem))
end
Wie lässt sich das konkrete ggT-Programm 3.1 aus dem daraus abstrahierten Programm
3.55 zurück konkretisieren? Durch Einsetzen der Teile der ursprünglichen Rekursionsformel (3.4) S. 2
⎧p
ggT(p, q) = ⎨
⎩ggT (q, p mod q)
falls q = 0
sonst
in die entsprechenden Funktionen:
Programm 3.56
Funktionaler
Pseudocode: ggT als
Konkretisierung
GCD_PROBLEM extends DIVISIBLE_PROBLEM is INTEGER × INTEGER
GCD_SOLUTION extends SOLUTION is NATURAL
directly_solvable ((p, q) : GCD_PROBLEM) : BOOLEAN is q = 0
direct_solution ((p, q) : GCD_PROBLEM) : GCD_SOLUTION is abs (p)
part ((p, q) : GCD_PROBLEM) : GCD_PROBLEM is (q, p mod q)
solution ((p, q) : GCD_PROBLEM) : GCD_SOLUTION is inherited
Die folgenden Umformungsschritte bieten sich an:
Umformungsschritte
(1)
(2)
(3)
Transformiere die abstrakte funktionale Lösung Programm 3.55 in eine abstrakte
objektorientierte Lösung. Benutze dazu Konzeptklassen und Schablonenmethoden für partielle Implementation.
Transformiere in der abstrakten objektorientierten Lösung den endrekursiven
Algorithmus in einen strukturierten iterativen Algorithmus.
Konkretisiere die abstrakte objektorientierte Lösung zu einer Lösung des ggTProblems.
Wir führen zu Schritt (1) in 3.3.2 einen sprachunabhängigen Entwurf aus, implementieren diesen in 3.3.3 in Eiffel und führen damit Schritt (2) aus, führen dann zu Schritt (3)
in 3.3.4 wieder einen sprachunabhängigen Entwurf aus, den wir in 3.3.5 in Eiffel implementieren. Dann evaluieren wir das Erreichte und planen weitere Schritte.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 38
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
3.3.2
Objektorientierter Entwurf der abstrakten Problemlösung nur mit Abfragen
Bild 3.3 Konzeptklassen für rekursiv teilbares Problem nur mit Abfragen
PROBLEM abstract generic
query
SOLUTION generic param.
solution abstract
DIVISIBLE_PROBLEM_Q abstract generic
query
directly_solvable
direct_solution
part
DIVISIBLE_PROBLEM_QR abstract generic
query
abstract
abstract
abstract
DIVISIBLE_PROBLEM_QI abstract generic
solution -- template recursive
query
solution -- template iterative
Der in Programm 3.55 erscheinende Typ DIVISIBLE_PROBLEM wird zu einer völlig abstrakten Konzeptklasse DIVISIBLE_PROBLEM_Q. Die Funktionen von Programm 3.55,
die alle einen DIVISIBLE_PROBLEM-Parameter haben, werden zu abstrakten parameterlosen Abfragen von DIVISIBLE_PROBLEM_Q. Wir brauchen DIVISIBLE_PROBLEM_Q als
Abstraktion, um die Abfrage solution in Nachfolgerklassen unterschiedlich implementieren zu können. In den Nachfolgerklassen DIVISIBLE_PROBLEM_QR und
DIVISIBLE_PROBLEM_QI wird solution als Schablonenmethode mittels Aufrufen der abstrakten Abfragen directly_solvable, direct_solution, part implementiert, und zwar mit
einem rekursiven bzw. iterativen Algorithmus.
3.3.3
Eiffel – Konzeptklassen
aus Bild 3.3 wäre als Konzeptklasse leer, die Abfrage solution in PROBLEM
wäre in konkreten Erweiterungsklassen kovariant an Erweiterungen von SOLUTION
anzupassen, was sprachabhängig zu Komplikationen führen kann. Geschickter ist,
SOLUTION als generischen Parameter der abstrakten generischen Klasse PROBLEM zu
realisieren.
SOLUTION
Programm 3.57
Eiffel: Generische
Konzeptklasse für
Problem
note
description: "Concept class: Abstract problem that has a generic solution"
deferred class PROBLEM [S]
feature
solution : S
deferred
ensure
solution_exists: Result /= Void
end
end -- class PROBLEM
Export
Die abstrakte Klasse DIVISIBLE_PROBLEM_Q von Programm 3.58 exportiert die neuen
Abfragen nur an sich selbst, denn Kunden wie PROBLEM_SOLVER brauchen sie nicht
zu kennen. Warum an sich selbst? In Eiffel ist die Schutzeinheit das Objekt, nicht die
Klasse. Wären die Abfragen durch
feature {NONE}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3 – 39
ganz geschützt, dürfte ein Objekt nur sich selbst danach abfragen. Damit ließe sich nur
die rekursive Implementationsvariante realisieren. Durch
feature {DIVISIBLE_PROBLEM_Q}
kann ein Objekt auch andere Objekte abfragen – diese Eigenschaft verlangt die iterative Implementationsvariante.
Verankerter Typ
Die Abfrage part soll ein Teilproblem derselben Art liefern, doch hier in der Konzeptklasse ist die konkrete Art noch unbekannt. Die Vereinbarung
part : DIVISIBLE_PROBLEM_Q
wäre zu schwach, da part im Nachfolger X von DIVISIBLE_PROBLEM_Q ein Objekt des
Nachfolgers Y von DIVISIBLE_PROBLEM_Q liefern dürfte. Als Typ von part wird kein
absoluter Typ benötigt, sondern ein relativer Typ, der sich mit der Vererbung an den
Typ des aktuellen Objekts anpasst. Dies lässt sich mit dem Eiffel-Konstrukt des verankerten Typs (anchored type) ausdrücken. Durch die Vereinbarung
part : like Current
ist der Typ von part in allen Nachfolgerklassen gleich dem Typ des aktuellen Objekts
Current.
Programm 3.58
Eiffel: Generische
Konzeptklasse für
teilbares Problem mit
Abfragen
note
description: "Concept class: Abstract divisible problem with query interface"
deferred class DIVISIBLE_PROBLEM_Q [S]
inherit
PROBLEM [S]
feature {DIVISIBLE_PROBLEM_Q}
directly_solvable : BOOLEAN
deferred
end
direct_solution : like solution
require
simple_case:
directly_solvable
deferred
ensure
dircect_solution_exists: Result /= Void
end
part : like Current
require
recursive_case: not directly_solvable
deferred
ensure
part_exists:
Result /= Void
end
end -- class DIVISIBLE_PROBLEM_Q
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 40
Programm 3.59
Eiffel: Generische
Konzeptklasse für
endrekursiv gelöstes
Problem
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
note
description: "Concept class: Divisible problem, query interface, recursive template"
deferred class DIVISIBLE_PROBLEM_QR [S]
inherit
DIVISIBLE_PROBLEM_Q [S]
feature
solution : S
-- Template query in tail-recursive form.
do
if directly_solvable then
Result := direct_solution
else
Result := part.solution
end
end -- solution
end -- class DIVISIBLE_PROBLEM_QR
Den endrekursiven Algorithmus von solution in Programm 3.59 nach dem Umformungsschema von S. 5 in einen iterativen Algorithmus umzuformen, erfordert zwei
Vorbereitungsschritte:
Das aktuelle Objekt ist ein impliziter Parameter des rekursiven Aufrufs. Um ihn
explizit zu machen, ist auf das aktuelle Objekt mit einer lokalen Referenz (z.B.
namens problem) zuzugreifen.
Eiffel kennt keine Sprunganweisungen; dynamische Enden von Routinen fallen stets
mit ihrem statischen Ende zusammen. Die möglichen dynamischen Enden in den
Zweigen der Auswahlanweisung sind zu markieren (z.B. mit return-Anweisungen
als Kommentare, die später eliminiert werden).
Programm 3.60
Eiffel:
Schablonenmethode
zu endrekursiv
gelöstem Problem,
umformungsbereit
solution : S
local
problem : like Current
do
problem := Current
if problem.directly_solvable then
Result := problem.direct_solution -- return
else
problem := problem.part
Result := problem.solution -- return
end
end -- solution
Der rekursive Aufruf
problem.solution
hat die funktionale Form
solution (problem)
Da problem der einzige Parameter ist, können die Schritte (1) bis (3) des Umformungsschemas von S. 5 – also die Vereinbarung einer lokalen Variablen, das Speichern des
Aktualparameters und das Rückspeichern in den Formalparameter – entfallen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
Programm 3.61
Pseudo-Eiffel:
Schablonenmethode
zu endrekursiv
gelöstem Problem,
umgeformt
3 – 41
solution : S
local
problem : like Current
do
problem := Current
loop
if problem.directly_solvable then
Result := problem.direct_solution -- return
else
problem := problem.part
Result := problem.solution -- return
end
end
end -- solution
Nun ist nur noch die unbedingte Schleife (die es in Eiffel nicht gibt) in die EiffelSchleife umzuformen und die Schablonenmethode in die Konzeptklasse
DIVISIBLE_PROBLEM_QI einzubauen.
Programm 3.62
Eiffel: Generische
Konzeptklasse für
iterativ gelöstes
Problem
note
description: "Concept class: Divisible problem, query interface, iterative template"
deferred class DIVISIBLE_PROBLEM_QI [S]
inherit
DIVISIBLE_PROBLEM_Q [S]
feature
solution : S
-- Template query in iterative form.
local
problem : like Current
do
from
problem := Current
until problem.directly_solvable loop
problem := problem.part
end
Result := problem.direct_solution
end -- solution
end -- class DIVISIBLE_PROBLEM_QI
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 42
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Bild 3.4 Konzept-, Schnittstellen- und Implementationsklassen für ggT-Problem nur mit Abfragen
PROBLEM abstract generic
query
SOLUTION generic param.
solution abstract
DIVISIBLE_PROBLEM_Q abstract generic
query
directly_solvable
direct_solution
part
abstract
abstract
abstract
binden
DIVISIBLE_PROBLEM_QR abstract generic
query
DIVISIBLE_PROBLEM_QI abstract generic
query
solution -- template recursive
solution -- template iterative
alternativ erben
GCD_PROBLEM_Q
3.3.4
abstract
GCD_SOLUTION
query
p
q
query
gcd
init_action
make
init_action
make
GCD_PROBLEM_Q1
GCD_PROBLEM_Q2
query
query
r
s
directly_solvable
direct_solution
part
init_action
make redefined
directly_solvable
direct_solution
part
Objektorientierter Entwurf der konkreten Problemlösung
Wie lässt sich mit den Konzeptklassen die Lösung des ggT-Problems beschreiben?
Zunächst sind die Schnittstellen um Dienste des konkreten Problems zu erweitern. Die
so erhaltenen noch abstrakten Schnittstellenklassen lassen sich dann durch Implementationsklassen konkretisieren. Bild 3.4 enthält im oberen Teil Bild 3.3, um den Entwurf
als Ganzes sichtbar zu machen.
3.3.5
Eiffel
3.3.5.1
Schnittstellenklassen
Die ggT-Lösungsklasse ist so einfach, dass sie sofort ohne Variation implementiert
wird. Sie ist zugleich Schnittstellen- und Implementationsklasse.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
Programm 3.63
Eiffel: Lösung zum
ggT-Problem
3 – 43
note
description: "Interface and implementation class: Solution of gcd problem"
class GCD_SOLUTION
create
{GCD_PROBLEM_Q} make
feature
gcd : INTEGER
feature {GCD_PROBLEM_Q}
make (new_gcd : INTEGER)
require
argument_nonnegative: new_gcd >= 0
do
gcd := new_gcd
ensure
argument_accepted:
gcd = new_gcd
end -- make
invariant
gcd_valid:
gcd >= 0
end -- class GCD_SOLUTION
Erfüllt GCD_SOLUTION die Anforderungen von S. 36? Die Exportbeschränkungen
Anforderung (2) S. 36
Anforderung (3) S. 36
Anforderung (4) S. 36
create {GCD_PROBLEM_Q} make
feature {GCD_PROBLEM_Q} make ...
für make garantieren, dass nur Nachfolger von GCD_PROBLEM_Q Objekte von
GCD_SOLUTION erzeugen und Lösungswerte new_gcd einstellen können. Da make nicht
nur Erzeugungsprozedur, sondern auch als normale Routine benutzbar ist, können
Nachfolger von GCD_PROBLEM_Q ihr einmalig erzeugtes GCD_SOLUTION-Objekt für
nacheinander berechnete Lösungswerte wiederverwenden. Der Lösungswert ist durch
feature
gcd : INTEGER
als öffentliches Attribut vereinbart, das kundenseitig nur lesbar, nicht veränderbar ist.
Als ggT-Problemklassen definieren wir eine Abstraktion mit mehreren Konkretisierungen. Da das ggT-Problem durch die Zahlen p und q beschrieben ist, genügt es, für sie in
der Abstraktion Abfragen und eine Prozedur make zum Setzen der Werte vorzusehen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 44
Programm 3.64
Eiffel: ggT-Problem,
abstrakt
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
note
description: "Interface class: Problem part of gcd problem, query interface"
deferred class GCD_PROBLEM_Q
inherit
DIVISIBLE_PROBLEM_QI [GCD_SOLUTION]
-- or DIVISIBLE_PROBLEM_QR
feature
p, q : INTEGER
make (new_p, new_q : INTEGER)
do
p := new_p
q := new_q
ensure
new_p_accepted: p = new_p
new_q_accepted: q = new_q
end -- make
invariant
gcd_bounded_by_p: p /= 0 implies solution.gcd <= p.abs
gcd_bounded_by_q: q /= 0 implies solution.gcd <= q.abs
end -- class GCD_PROBLEM_Q
3.3.5.2
Implementationsklassen
Die erste Konkretisierung der Problemklasse erklärt make zur Erzeugungsprozedur und
definiert die von DIVISIBLE_PROBLEM_Q geerbten abstrakten Abfragen direkt.
Programm 3.65
Eiffel: ggT-Problem
Variante 1
note
description: "Implementation: Gcd problem, query interface, straightforward"
class GCD_PROBLEM_Q1
inherit
GCD_PROBLEM_Q
create
make
feature {GCD_PROBLEM_Q1}
directly_solvable : BOOLEAN
do
Result := q = 0
end
direct_solution : like solution
do
create Result.make (p.abs)
end
part : like Current
do
create Result.make (q, p \\ q)
end
end -- class GCD_PROBLEM_Q1
☺
Programm 3.65 bietet eine kompakte, elegante Problemlösung, von deren Korrektheit
man sich leicht überzeugt, da die Teile der Rekursionsformel (3.4) S. 2
⎧p
ggT(p, q) = ⎨
⎩ggT (q, p mod q)
falls q = 0
sonst
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
Anforderung (1) S. 36
3 – 45
direkt zu erkennen sind. Es erfüllt die Anforderung (1) S. 36, da make nicht nur beim
Erzeugen, sondern auch zum Reinitialisieren benutzbar ist. Aber leider ist Programm
3.65 ineffizient, weil part durch
create Result.make (q, p \\ q)
für jedes Teilproblem ein neues Objekt dynamisch erzeugt. Auch das von solution aufgerufene direct_solution erzeugt durch
create Result.make (p.abs)
Anforderung (3) S. 36
jedesmal ein neues Objekt, was der Anforderung (3) S. 36 widerspricht. Objekterzeugungen wollten wir durch die iterative Implementation gerade vermeiden. Verlieren wir
etwa mit der Abstraktion zwangsläufig an Effizienz?
Nein! Die erste Idee, die Problem- und Lösungsklassen zu expandieren, damit part,
solution und direct_solution mit Wert- statt Referenzobjekten arbeiten, verwerfen wir
allerdings weil Funktionen, die Wertobjekte zurückgeben, kaum effizient zu implementieren sind. Stattdessen versuchen wir, die Lösungselemente der iterativen Variante
geeignet in die Implementationsklasse einzubringen, nämlich die Variablen r, s, t, mit
denen die Berechnung durchzuführen ist.
Programm 3.66
Eiffel: ggT-Problem
Variante 2
note
description: "Implementation: Gcd problem, query interface, hidden side-effects"
class GCD_PROBLEM_Q2
inherit
GCD_PROBLEM_Q
redefine
make
end
create
make
feature {NONE}
r, s : INTEGER
feature
make (new_p, new_q : INTEGER)
do
Precursor (new_p, new_q)
r := p.abs
s := q.abs
if direct_solution = Void then
create direct_solution.make (0)
end
ensure then
direct_solution_exists: direct_solution /= Void
end -- make
feature {GCD_PROBLEM_Q2}
directly_solvable : BOOLEAN
do
if s = 0 then
direct_solution.make (r)
Result := True
end
end -- directly_solvable
direct_solution : like solution
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 46
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
part : like Current
local
t : INTEGER
do
t := r \\ s
r := s
s := t
Result := Current
end -- part
end -- class GCD_PROBLEM_Q2
Redefinition
Der redefine-Abschnitt ist ein Unterabschnitt des inherit-Abschnitts. Hier sind die
Namen der Merkmale aufzulisten, die in Vorgängern schon implementiert sind und in
dieser Klasse eine neue Implementation erhalten. Bisher aufgeschobene Merkmale dürfen im redefine-Abschnitt nicht genannt werden.
In Programm 3.66 werden die Berechnungsvariablen r, s als geschützte Attribute vereinbart, in make initialisiert, in part für einen Berechnungsschritt benutzt, und in
directly_solvable ausgewertet. direct_solution wird als Attribut vereinbart, einmal in make
erzeugt und provisorisch initialisiert, und von directly_solvable mit dem Lösungswert
versehen. part muss für das Teilproblem kein neues Objekt erzeugen, sondern gibt nur
das aktuelle Objekt zurück, das das Teilproblem in r, s selbst speichert.
☺
Nebeneffekt
Programm 3.66 ist effizienter als Programm 3.65 und erfüllt alle vier Anforderungen
von S. 36. Ineffizient bleibt es insofern, als solution das Ergebnis bei jedem Aufruf neu
berechnet, auch wenn p und q ihre Werte behalten haben. Als strukturelle Schwäche
werte ich, dass Programm 3.66 in den Abfragen directly_solvable, part und solution
Nebeneffekte enthält, die bekanntlich verpönt sind:
directly_solvable verändert den Zustandsteil des aktuellen Objekts, der mit
direct_solution abzufragen ist.
part verändert den Zustand des aktuellen Objekts durch Zuweisungen an die verborgenen Attribute r, s.
solution bewirkt indirekt Nebeneffekte, weil es directly_solvable und part aufruft.
Somit handelt es sich hier um versteckte Nebeneffekte, die kundenseitig nicht erkennbar sind, da direct_solution, r und s nur der Problemklasse GCD_PROBLEM_Q2 selbst
zugänglich sind. Hinter der öffentlichen Schnittstelle verborgene Nebeneffekte sind
weniger problematisch als öffentlich wahrnehmbare. Trotzdem ist Programm 3.66
wegen der Nebeneffekte weniger klar und schwerer verifizierbar als Programm 3.65.
3.3.6
Objektorientierter Entwurf mit Abfragen und Aktionen
Deshalb suchen wir eine nebeneffektfreie, bessere Lösung – freilich wieder erst in
einem sprachunabhängigen Entwurf. Für jede nebeneffektbehaftete Abfrage von Programm 3.66 definieren wir eine Aktion, die den entsprechenden Effekt bewirkt:
check_solvability
divide
compute_solution
zu
zu
zu
directly_solvable,
part,
solution.
directly_solvable lässt sich dann als Attribut fertig implementieren. part, das nur das aktuelle Objekt zurückgibt, entfällt ganz. solution bleibt, da es von der Konzeptklasse PROBLEM abstammt. Somit ist die neue Schnittstelle gegenüber der abfrageorientierten um
einen Dienst breiter.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
Bild 3.5
Konzept- und
Implementationsklassen für ggTProblem mit Abfragen
und Aktionen
PROBLEM abstract generic
query
3 – 47
SOLUTION generic param.
solution abstract
DIVISIBLE_PROBLEM_QAI abstract generic
query
directly_solvable
action
check_solvability
abstract
divide
abstract
compute_solution -- template iterative
binden
GCD_PROBLEM_QAI
p
q
r
s
solution
query
init_action
action
3.3.7
make
check_solvability
divide
GCD_SOLUTION
query
gcd
init_action
make
Eiffel
Da in Eiffel Aktionen Kommandos heißen, verwenden wir statt QAI die Abkürzung
QCI.
3.3.7.1
Programm 3.67
Eiffel: Konzeptklasse
für iterativ-imperativ
gelöstes Problem
Konzeptklasse
note
description: "Concept class: Divisible problem, query-command interface, iterative"
deferred class DIVISIBLE_PROBLEM_QCI [S]
inherit
PROBLEM [S]
feature {NONE}
directly_solvable : BOOLEAN
check_solvability
-- Update directly_solvable.
deferred
end
divide
require
recursive_case: not directly_solvable
deferred
end
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 48
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
compute_solution
-- Template command.
do
from
check_solvability
until directly_solvable loop
divide
check_solvability
end
end -- compute_solution
end -- class DIVISIBLE_PROBLEM_QCI
Da bei dieser iterativen Variante alle Merkmale nur noch vom aktuellen Objekt
gebraucht werden, schränken wir ihren Exportstatus ein.
3.3.7.2
Implementationsklassen
In der Lösungsklasse GCD_SOLUTION Programm 3.63 ist nur GCD_PROBLEM_QCI in
die Kundenliste von make aufzunehmen.
Programm 3.68
Eiffel: Lösung zum
ggT-Problem, erweitert
class GCD_SOLUTION
-- Stuff not shown same as in Programm 3.63.
create
{GCD_PROBLEM_Q, GCD_PROBLEM_QCI} make
feature {GCD_PROBLEM_Q, GCD_PROBLEM_QCI}
-- Stuff not shown same as in Programm 3.63.
end -- class GCD_SOLUTION
Programm 3.69
Eiffel: ggT-Problem
Variante 3
note
description: "Implementation: Gcd problem, query-command interface, iterative"
class GCD_PROBLEM_QCI
inherit
DIVISIBLE_PROBLEM_QCI [GCD_SOLUTION]
redefine
solution
end
create
make
feature
p, q : INTEGER
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3 – 49
make (new_p, new_q : INTEGER)
do
p := new_p
q := new_q
r := p.abs
s := q.abs
directly_solvable := False
if solution = Void then
create solution.make (0)
end
compute_solution
ensure
new_p_accepted: p = new_p
new_q_accepted: q = new_q
solution_exists:
solution /= Void
end -- make
solution : GCD_SOLUTION
feature {NONE}
r, s : INTEGER
check_solvability
do
if s = 0 then
directly_solvable := True
solution.make (r)
end
end -- check_solvability
divide
local
t : INTEGER
do
t := r \\ s
r := s
s := t
end -- divide
end -- class GCD_PROBLEM_QCI
Evaluation
Programm 3.69
☺ ist effizienter als Programm 3.66,
☺ erfüllt alle vier Anforderungen von S. 36,
☺ solution liefert ein gespeichertes Ergebnis, das nur dann von make neu berechnet
wird, wenn p und q neue Werte erhalten,
☺ kommt ohne die versteckten Nebeneffekte von Programm 3.66 aus,
gewinnt gegenüber Programm 3.66 an Klarheit, erreicht jedoch nicht die Eleganz
von Programm 3.65.
3.3.7.3
Programm 3.70
Eiffel: ggT-Rechner als
Problemlöser
Kundenklasse
note
description: "Command-line calculator for greatest common divisor of integer pairs"
class GCD_PROBLEM_SOLVER
create
make
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 50
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
feature {NONE}
gcd_q1 : GCD_PROBLEM_Q
once
create {GCD_PROBLEM_Q1} Result.make (0, 0)
ensure
gcd_q1_exists: Result /= Void
end
gcd_q2 : GCD_PROBLEM_Q
once
create {GCD_PROBLEM_Q2} Result.make (0, 0)
ensure
gcd_q2_exists: Result /= Void
end
gcd_qci : GCD_PROBLEM_QCI
once
create Result.make (0, 0)
ensure
gcd_qci_exists: Result /= Void
end
write (name : STRING; p, q, gcd : INTEGER)
do
io.put_string ("gcd_" + name + " (" + p.out + ", " + q.out + ") = " + gcd.out)
io.put_new_line
end -- write
make (arguments : ARRAY [STRING])
-- Given integer pairs as command arguments, print their greatest common divisor,
-- computed by 3 variants.
local
i, p, q : INTEGER
do
if arguments = Void or else arguments.count < 3 then
io.put_string
("Please call the gcd problem solver with integer pairs as arguments!")
io.put_new_line
else
from
i := 2
until arguments.count <= i or else
not arguments.item (i - 1).is_integer or else
not arguments.item (i).is_integer
loop
p := arguments.item (i - 1).to_integer
q := arguments.item (i).to_integer
gcd_q1.make (p, q)
gcd_q2.make (p, q)
gcd_qci.make (p, q)
write ("q1 ", gcd_q1.p, gcd_q1.q, gcd_q1.solution.gcd)
write ("q2 ", gcd_q2.p, gcd_q2.q, gcd_q2.solution.gcd)
write ("qci", gcd_qci.p, gcd_qci.q, gcd_qci.solution.gcd)
io.put_new_line
i := i + 2
end
end
end -- make
end -- class GCD_PROBLEM_SOLVER
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3.3.8
3 – 51
C++
Für die drei C*-Sprachen folgt nur die Umsetzung des letzten Entwurfs 3.3.6.; die Entwürfe 3.3.2 und 3.3.4 setzen Sie als Übung um (s. Aufgabe 3.16). Für die C*-Implementationsvarianten gilt dieselbe Bewertung wie für die Eiffel-Variante auf S. 49,
sodass im Folgenden Bemerkungen zu sprachlichen Besonderheiten genügen.
3.3.8.1
Konzeptklassen
Die Lösungsklasse realisieren wir auch in C++ als generischen Parameter S der abstrakten generischen Problemklasse Problem:
Generische Klasse
template <class S> class Problem
C++ bezeichnet Generizität als Schablone (template); den Begriff sollte man nicht mit
dem objektorientierten Entwurfsmuster Schablonenmethode (template method) verwechseln! Übrigens kennt C++ als hybride Sprache nicht nur generische Klassen, sondern auch generische Funktionen.
Programm 3.71
C++: Generische
Konzeptklasse für
Problem
// Header file: Problem.hpp
#ifndef PROBLEM_HPP_
#define PROBLEM_HPP_
template <class S> class Problem {
public:
virtual S & solution () const = 0;
};
#endif // PROBLEM_HPP_
Während Eiffel bei abstrakten parameterlosen Abfragen offen lässt, ob sie später als
Attribute oder Funktionen zu implementieren sind, müssen sie in den C*-Sprachen als
abstrakte Funktionen deklariert werden. Da aber Attribute nie öffentlich sein, sondern
ggf. durch öffentliche Lesezugriffsfunktionen ergänzt werden sollen, liegt darin kein
zusätzlicher Nachteil.
Programm 3.72
C++: Generische
Konzeptklasse für
iterativ-imperativ
gelöstes Problem,
Schnittstelle
// Header file: DivisibleProblemQAI.hpp
#ifndef DIVISIBLE_PROBLEM_QAI
#define DIVISIBLE_PROBLEM_QAI
#include "Problem.hpp"
template <class S> class DivisibleProblemQAI : public Problem<S> {
protected:
bool directly_solvable;
virtual void check_solvability () = 0;
virtual void divide () = 0;
// Precondition: !directly_solvable
virtual void compute_solution ();
};
template <class S> void DivisibleProblemQAI<S>::compute_solution () {
while (true) {
check_solvability();
if (directly_solvable) break;
divide();
}
}
#endif // DIVISIBLE_PROBLEM_QAI
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 52
Implementation in der
Schnittstelle
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Normalerweise enthält eine Schnittstellendatei nur die Deklarationen der exportierten
Klassen (Funktionen). Doch bei generischen Klassen (Funktionen) muss die Schnittstellendatei auch die Implementationen der Klassen (Funktionen) vollständig enthalten,
denn nur so kann der Kompilierer an inkludierenden Stellen Code für Konkretisierungen der Klassen (Funktionen) erzeugen. Zwischen den Klassenklammern steht nur die
Schnittstelle der Klasse. Implementationen von Klassen- und Elementfunktionen stehen außerhalb davon, normalerweise in der zugehörigen Implementationsdatei, aber
bei generischen Klassen folgen sie in der Schnittstellendatei nach dem Klassentext.
Der iterative Algorithmus von compute_solution lässt sich mit einer Bedingungsschleife
oder einer unbedingten Schleife mit bedingtem Sprung aus dem Schleifenrumpf formulieren. Es handelt sich um eine „n+1/2-Schleife“ (Dijkstra). Analysieren wir beide Varianten:
Bedingungsschleife
Evaluation
check_solvability();
while (!directly_solvable) {
divide();
check_solvability();
}
☺ Streng strukturierte Schleife, da ein Eingang, ein Ausgang; mit Struktogramm dar-
stellbar.
☺ Schachtelungstiefe der Anweisungen: 2.
Aufruf von check_solvability an zwei Stellen.
Unbedingte Schleife
mit Sprung aus dem
Rumpf
Evaluation
Fazit
3.3.8.2
while (true) {
check_solvability();
if (directly_solvable) {
break;
} else {
divide();
}
}
for (;;) {
check_solvability();
if (directly_solvable) break;
divide();
}
Schwach strukturierte Schleife, da sie aus der Rumpfmitte verlassen wird; hat zwar
nur je einen Ein- und Ausgang, ist aber nicht mit einem Struktogramm darstellbar.
Schachtelungstiefe der Anweisungen: 3 wegen zusätzlichem if.
while- oder for-Schleife erforderlich, da die C*-Sprachen kein Konstrukt für unbedingte Schleifen haben. Hoffentlich setzt der Kompilierer diese Pseudo-Bedingungsschleife so um, dass der Ausdruck true nicht zur Laufzeit ausgewertet wird!
☺ Aufruf von check_solvability an nur einer Stelle.
Einziger Nachteil der Bedingungsschleife ist die Kopie einer Anweisung im Initialisierungsteil; einziger Vorteil der unbedingten Schleife ist, dass diese Anweisung nur einmal hinzuschreiben ist und nur einmal dafür Code erzeugt (d.h. ein Maschinenbefehl
gespart ;-) wird. Welche Variante ist vorzuziehen? Das hängt davon ab, wie man die
Vor- und Nachteile gewichtet. Da hier einer der raren Fälle vorliegt, in denen der Einsatz der break-Anweisung überhaupt einen Vorteil und nicht nur Nachteile bietet, wähle
ich die unbedingte Schleife mit dem bedingtem Sprung in einer Zeile.
Implementationsklassen
Die ggT-Lösungsklasse GcdSolution bietet als Behälter der Lösungszahl gcd nichts
Spektakuläres. Als Alternative zum scheußlichen get_-Präfix bei der Getter-Funktion
verwende ich hier die Konvention, die Lesezugriffsfunktion passend zu benennen und
das zugehörige Attribut genauso, aber mit dem hässlichen Präfix „_“ versehen.
Die Anforderung (2) S. 36, dass nur Problem-Klassen Solution-Objekte erzeugen und
Lösungswerte einstellen können, ist in C++ nicht erfüllbar, da dieser Schutzzustand
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3 – 53
zwischen public und protected liegt. Einen Konstruktor und die set-Methode mit protected zu schützen wäre zu stark, weil sie dann nur für Ableitungsklassen von GcdSolution
aufrufbar wären. Also muss set public sein; als Konstruktor genügt der öffentliche Standardkonstruktor. Damit kann aber jedes Programmteil GcdSolution-Objekte vereinbaren, erzeugen und ihren Zustand ändern.
Programm 3.73
C++: Lösung zum ggTProblem, Schnittstelle
// Header file: GcdSolution.hpp
#ifndef GCD_SOLUTION_HPP_
#define GCD_SOLUTION_HPP_
class GcdSolution {
protected:
int _gcd;
public:
int gcd () const {return _gcd;}
virtual void set (int new_gcd);
};
#endif // GCD_SOLUTION_HPP_
Programm 3.74
C++: Lösung zum ggTProblem,
Implementation
// Implementation file: GcdSolution.cpp
#include "GcdSolution.hpp"
#include <cassert>
void GcdSolution::set (int new_gcd) {
assert(new_gcd >= 0); // precondition
_gcd = new_gcd;
}
Die ggT-Problemklasse GcdProblemQAI liefert bei solution Anschauungsmaterial für das
Zusammenspiel von Zeigern und Referenzen in C++.
Programm 3.75
C++: ggT-Problem,
Schnittstelle
// Header file: GcdProblemQAI.hpp
#ifndef GCD_PROBLEM_QAI
#define GCD_PROBLEM_QAI
#include "DivisibleProblemQAI.hpp"
#include "GcdSolution.hpp"
class GcdProblemQAI : public DivisibleProblemQAI<GcdSolution> {
protected:
GcdSolution * _solution;
int _p, _q, r, s;
void check_solvability ();
void divide ();
public:
GcdSolution & solution () const {return *_solution;}
int p () const {return _p;}
int q () const {return _q;}
virtual void set (int new_p, int new_q);
GcdProblemQAI ();
virtual ~GcdProblemQAI ();
};
#endif // GCD_PROBLEM_QAI
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 54
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
Die doppelte Aufgabe, die make in Programm 3.69 als Erzeugungs- und Reinitialisierungsprozedur übernimmt, ist in den C*-Sprachen auf einen Konstruktor und eine Setter-Prozedur aufzuteilen. Im Konstruktor wird das GcdSolution-Objekt dynamisch auf
der Halde erzeugt.
Manuelle
Speicherverwaltung
Programm 3.76
C++: ggT-Problem,
Implementation
Da C++ keine automatische Speicherbereinigung hat, muss der Programmierer das
Löschen dynamisch erzeugter Bestandteile von Objekten selbst programmieren. Deshalb ist hier ein Destruktor ~GcdProblemQAI zu definieren, der das GcdSolution-Objekt
löscht, bevor das aktuelle GcdProblemQAI-Objekt gelöscht wird. Destruktoren sollen
stets virtual deklariert sein, damit ihr Aufruf in polymorphen Situationen nicht unterbleibt.
// Implementation file: GcdProblemQAI.cpp
#include "GcdProblemQAI.hpp"
#include <cassert>
#include <cstdlib>
void GcdProblemQAI::check_solvability () {
if (s == 0) {
directly_solvable = true;
_solution->set(r);
}
}
void GcdProblemQAI::divide () {
assert(!directly_solvable); // precondition
int t = r % s;
r = s;
s = t;
}
void GcdProblemQAI::set (int new_p, int new_q) {
_p = new_p;
_q = new_q;
r = abs(_p);
s = abs(_q);
directly_solvable = false;
compute_solution();
}
GcdProblemQAI::GcdProblemQAI () {
_solution = new GcdSolution();
}
GcdProblemQAI::~GcdProblemQAI () {
delete _solution;
}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3.3.8.3
Programm 3.77
C++: ggT-Rechner als
Problemlöser
3 – 55
Kundenfunktion
// Implementation file: GcdProblemSolver.cpp
#include "GcdProblemQAI.hpp"
#include <cstdlib>
#include <iostream>
int main (int argc, char * argv[]) {
if (argc < 3) {
std::cout << "Please call the gcd problem solver with integer pairs as arguments!";
<< std::endl;
} else {
GcdProblemQAI * gcd_problem = new GcdProblemQAI();
int p, q;
for (int i = 2; i < argc; i += 2) {
p = std::atoi(argv[i-1]);
q = std::atoi(argv[i]);
gcd_problem->set(p, q);
std::cout << "gcd(" << gcd_problem->p() << ", " << gcd_problem->q()
<< ") = " << gcd_problem->solution().gcd() << std::endl;
}
delete gcd_problem;
}
return 0;
}
3.3.9
Java
Das Bedürfnis, den Programmieraufwand zu beschränken (also Faulheit), verführt uns
dazu, das protected-Problem zu verdrängen, die eben postulierte Leitlinie 3.3 abzuschwächen, und alle Klassen des Entwurfs von Bild 3.5 in ein einziges Paket Problems
zu legen. Diese Disziplinlosigkeit hat ihre gute Seite, denn sie ermöglicht, die Anforderung (2) S. 36 näherungsweise zu erfüllen.
3.3.9.1
Konzeptklassen
In Java sind Interfaces (interface) wie rein abstrakte Klassen, die keinerlei Implementationsteile enthalten, also weder Attribute, noch Methodenrümpfe, noch Konstruktoren. Interfaces erlauben eine eingeschränkte Form von Mehrfachvererbung, da eine
Klasse nur eine andere Klasse beerben, aber beliebig viele Interfaces implementieren
darf. Interfaces können auch generisch sein. Da die Konzeptklasse Problem keine Implementationsteile enthält, bietet es sich an, sie in Java als Interface zu realisieren.
Programm 3.78
Java: Generisches
Konzept-Interface für
Problem
package Problems;
// Only to be used for extensions of Problem and Solution.
public interface Problem<S> {
S solution ();
}
DivisibleProblemQAI kann kein Interface sein, da es mit directlySolvable ein
Datenelement und mit computeSolution eine fertig implementierte Methode enthält.
Da directlySolvable in Implementationsklassen schreibbar sein muss, muss es
protected sein. Für Kundenklassen sollte es aber schreibgeschützt sein!
Da checkSolvability und divide in Implementationsklassen definierbar sein
müssen, müssen sie protected sein. Für Kundenklassen sollten sie aber aufrufgeschützt sein!
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 56
Programm 3.79
Java: Generische
Konzeptklasse für
iterativ-imperativ
gelöstes Problem
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
package Problems;
// Only to be used for extensions of Problem and Solution.
public abstract class DivisibleProblemQAI<S> implements Problem<S> {
protected boolean directlySolvable;
protected abstract void checkSolvability ();
protected abstract void divide ();
// Precondition: !directlySolvable
protected void computeSolution () {
while (true) {
checkSolvability();
if (directlySolvable) break;
divide();
}
}
}
3.3.9.2
Programm 3.80
Java: Lösung zum ggTProblem
Implementationsklassen
package Problems;
// Only to be used for extensions of Problem and Solution.
public class GcdSolution {
private int gcd;
public int gcd () {return gcd;}
protected void set (int gcd) {
assert gcd >= 0 : "precondition";
this.gcd = gcd;
}
protected GcdSolution () {}
}
Der protected Konstruktor und die protected set-Methode garantieren, dass nur
Klassen im Paket Problems GcdSolution-Objekte erzeugen und Lösungswerte einstellen können. Das entspricht fast der Anforderung (2) S. 36.
Programm 3.81
Java: ggT-Problem
package Problems;
// Only to be used for extensions of Problem and Solution.
public class GcdProblemQAI extends DivisibleProblemQAI<GcdSolution> {
private GcdSolution solution;
private int p, q, r, s;
protected void checkSolvability () {
if (s == 0) {
directlySolvable = true;
solution.set(r);
}
}
protected void divide () {
assert !directlySolvable : "precondition";
int t = r % s;
r = s;
s = t;
}
public GcdSolution solution () {return solution;}
public int p () {return p;}
public int q () {return q;}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3 – 57
public void set (int p, int q) {
this.p = p;
this.q = q;
r = Math.abs(p);
s = Math.abs(q);
directlySolvable = false;
computeSolution();
}
public GcdProblemQAI () {
solution = new GcdSolution();
}
}
3.3.9.3
Programm 3.82
Java: ggT-Rechner als
Problemlöser
Kundenklasse
class GcdProblemSolver {
protected static final String HINT =
"Please call the gcd problem solver" +
"with integer pairs as arguments! ";
public static void main (String[] args) {
if (args.length < 2) {
System.out.println("Missing Argument. " + HINT);
} else {
try {
GcdProblemQAI gcdProblem = new GcdProblemQAI();
int p, q;
for (int i = 1; i < args.length; i += 2) {
p = Integer.parseInt(args[i-1]);
q = Integer.parseInt(args[i]);
gcdProblem.set(p, q);
System.out.println
("gcd(" + gcdProblem.p() + ", " + gcdProblem.q() + ") = "
+ gcdProblem.solution().gcd());
}
} catch (Exception e) {
System.out.println("Wrong Argument. " + HINT + e.toString());
}
}
}
}
3.3.10
C#
3.3.10.1
Konzeptklassen
Programm 3.83
C#: Generische
Konzeptklasse für
Problem
abstract class Problem<S> {
public abstract S Solution ();
}
C# ist bei Interfaces strenger als Java: Da wir Solution in der direkten Ableitungsklasse DivisibleProblemQAI noch nicht implementieren wollen, kann Problem kein
Interface, sondern muss eine abstrakte Klasse sein. Solution muss eine abstrakte
Funktion sein, da Properties nicht abstrakt sein können.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 58
Programm 3.84
C#: Generische
Konzeptklasse für
iterativ-imperativ
gelöstes Problem
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
abstract class DivisibleProblemQAI<S> : Problem<S> {
protected bool directlySolvable;
protected abstract void CheckSolvability ();
protected abstract void Divide ();
// Precondition: !directlySolvable
protected virtual void ComputeSolution () {
while (true) {
CheckSolvability();
if (directlySolvable) break;
Divide();
}
}
}
3.3.10.2
Programm 3.85
C#: Lösung zum ggTProblem
Implementationsklassen
class GcdSolution {
protected int gcd;
public int Gcd {get {return gcd;}}
protected internal void Set (int gcd) {
System.Diagnostics.Debug.Assert (gcd >= 0, "precondition");
this.gcd = gcd;
}
protected internal GcdSolution () {}
}
Die Schutzmodifikatoren protected internal schränken die Kundenmenge der Setter-Methode und des Konstruktors so weit möglich ein, nämlich auf Ableitungsklassen,
die in derselben „assembly“ oder „dynamic link library“ wie die Basisklasse liegen.
Programm 3.86
C#: ggT-Problem
class GcdProblemQAI : DivisibleProblemQAI<GcdSolution> {
protected GcdSolution solution;
protected int p, q, r, s;
protected override void CheckSolvability () {
if (s == 0) {
directlySolvable = true;
solution.Set(r);
}
}
protected override void Divide () {
System.Diagnostics.Debug.Assert
(!directlySolvable, "precondition");
int t = r % s;
r = s;
s = t;
}
public override GcdSolution Solution () {return solution;}
public int P {get {return p;}}
public int Q {get {return q;}}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden
3 – 59
public virtual void Set (int p, int q) {
this.p = p;
this.q = q;
r = System.Math.Abs(p);
s = System.Math.Abs(q);
directlySolvable = false;
ComputeSolution();
}
public GcdProblemQAI () {
solution = new GcdSolution();
}
}
3.3.10.3
Programm 3.87
C#: ggT-Rechner als
Problemlöser
Kundenklasse
class GcdProblemSolver {
protected const string Hint =
"Please call the gcd problem solver
with integer pairs as arguments! ";
public static void Main (string[] args) {
if (args.Length < 2) {
System.Console.WriteLine("Missing Argument. " + Hint);
} else {
try {
GcdProblemQAI gcdProblem = new GcdProblemQAI();
int p, q;
for (int i = 1; i < args.Length; i += 2) {
p = System.Convert.ToInt32(args[i-1]);
q = System.Convert.ToInt32(args[i]);
gcdProblem.Set(p, q);
System.Console.WriteLine
("gcd(" + gcdProblem.P + ", " + gcdProblem.Q + ") = " +
gcdProblem.Solution().Gcd);
}
} catch (System.Exception e) {
System.Console.WriteLine
("Wrong Argument. " + Hint + e.Message);
}
}
}
}
3.3.11
Fazit
Die Entwürfe 3.3.2, 3.3.4 und 3.3.6 abstrakter und konkretisierter Problemlösungen mit
Konzeptklassen und Schablonenmethoden lassen sich in allen Auswahlsprachen ähnlich realisieren. Die Sprachvarianten unterscheiden sich nur in Details. Konzeptklassen
realisiert man als
abstrakte Klassen (Component Pascal, Eiffel, C*),
Interfaces, wenn sie rein abstrakt sind (Java, C#),
Die Problemklasse lässt sich in Eiffel und C* vorteilhaft generisch definieren. Den iterativen Algorithmus der Schablonenmethode implementiert man
nur rein strukturiert (Eiffel),
oder auch mit einem bedingten Sprung aus einer unbedingten Schleife (Component
Pascal, C*).
Das Erzeugen von Objekten von Lösungsklassen lässt sich
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
3 – 60
3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt
auf Problemklassen einschränken (Component Pascal, Eiffel),
nicht auf Problemklassen einschränken (C*).
Die Aufgaben Initialisierung und Reinitialisierung
lassen sich mit einer einzigen Prozedur erledigen (Component Pascal, Eiffel),
sind auf einen Konstruktor und eine Setter-Prozedur zu verteilen (C*).
3.3.12
Aufgaben
Aufgabe 3.16
Entwürfe 3.3.2, 3.3.4
Implementieren Sie die Entwürfe 3.3.2 und 3.3.4 in C++, Java und C#!
Aufgabe 3.17
ggT- und kgVProblemlöser
Sprachen: C*.
(1)
(2)
Testen Sie alle Klassen und den ggT-Problemlöser nach 3.3!
Ergänzen Sie
die Lösungsklasse um eine Abfrage für das kleinste gemeinsame Vielfache,
die Problemklasse um die Berechnung des kgV,
den ggT-Problemlöser um die Ausgabe des kgV der eingegebenen Zahlen!
Sprachen: Alle.
Aufgabe 3.18
Stellenzahl,
konkretisierte
Abstraktion
Entwickeln Sie für die Probleme aus Aufgabe 3.6 und Aufgabe 3.14 Lösungen, die die
abstrakten Lösungen aus 3.3 konkretisieren!
Sprachen: Alle.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4
Reihungen und Abstraktionen
Aufgabe
Beispiel
4 Easter
Bild
4 4 1961,
(4) aLeitlinie
4on ALGOL
4 60 wasTabelle
4 in Brighton, England, with
Around
courseProgramm
offered
Peter Naur, Edsger W. Dijkstra, and Peter Landin as tutors. [...] It was there that I
first learned about recursive procedures and saw how to program the sorting
method which I had earlier found such difficulty in explaining. It was there that I
wrote the procedure, immodestly named Quicksort, on which my career as a computer scientist is founded.
C. A. R. Hoare1
Dieses Kapitel befasst sich in Programmbeispielen mit Reihungen und wichtigen Suchund Sortieralgorithmen. Dabei nutzt es die Abstraktionskonzepte Generizität und Vererbung und zeigt, wie man die Trennung von Anwendungen, wiederverwendbaren
Testtreibern und funktionalen Einheiten mit Entwurfsmustern wie der Schablonenmethode erzielen kann.
4.1
Binäres Suchen in einer sortierten Reihung
Bei den folgenden Problemen kommen Reihungen ins Spiel. Das erste Problem ist das
Prüfen, ob eine Reihung sortiert ist.
Problem
Sortiertprüfung
Gegeben:
Eine Reihung array von Elementen mit einer Vollordnungsrelation ≤.
Gesucht:
Ist die Reihung sortiert, d.h. gilt array[first] ≤ ... ≤ array[last]?
Mathematisch handelt es sich wie bei der ggT-Funktion um eine Abbildung, etwa
sorted : Gn → lB.
Informatisch handelt es sich um einen Spezialfall des Suchproblems, das der Krimskrams-Beitrag Folgen durchlaufen untersucht. Die passende Lösung ist eine boolesche
Funktion mit der Reihung als Parameter, implementiert mit einem rekursiven oder iterativen Algorithmus. Das zweite Problem ist das Suchen eines Elements in einer sortierten Reihung.
Problem Suchen
Gegeben:
Eine sortierte Reihung array von Elementen mit einer Vollordnungsrelation und ein mögliches Element item.
Gesucht:
(a)
Ist das Element item in der Reihung enthalten, d.h. gibt es einen
Index i mit array[i] = item?
(b)
Falls das Element item in der Reihung enthalten ist, ein Index i mit
array[i] = item, sonst not_found.
Mathematisch handelt es sich wieder um Abbildungen, etwa
has : Gn × G → lB
bzw.
Lösungen
index : Gn × G → {1,.., n, not_found}.
Algorithmische Standardlösungen sind lineares und binäres Suchen.
Lineares Suchen geht inkrementell vor: Durchlaufe die Reihung elementweise vom
Anfang bis das gesuchte Element gefunden oder das Ende erreicht ist. Jeder Schritt
1
C. A. R. Hoare: The Emperor’s Old Clothes. ACM Turing Award Lecture 1980. Communications of the ACM, Vol. 24, No. 2 (February 1981) S. 75-83
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 4 – Seite 1 von 46
4–2
Komplexität
4.1.1
4 Reihungen und Abstraktionen
verkleinert den Suchraum um ein Element. Lineares Suchen behandelt der Krimskrams-Beitrag Folgen durchlaufen.
Binäres Suchen setzt eine sortierte Reihung voraus und nutzt das Teile-und-herrsche-Prinzip: Vergleiche das gesuchte Element mit dem mittleren Element der Reihung. Ist das gesuchte Element kleiner, suche in der linken Hälfte, ist es größer,
suche in der rechten Hälfte weiter, bis es gefunden oder der Suchraum leer ist. Jeder
Schritt halbiert den Suchraum. Binäres Suchen behandelt dieser Abschnitt.
Bei sortierter Reihung ist das effizientere binäre Suchen mit einem Laufzeitaufwand
der Größenordnung O(log(n)) dem linearen Suchen mit einem Laufzeitaufwand der
Größenordnung O(n) vorzuziehen. Die passende Lösung ist eine Funktion mit der sortierten Reihung und dem gesuchten Element als Parameter, implementiert mit einem
rekursiven oder iterativen Algorithmus.
Trennen wiederverwendbarer Komponenten von Anwendungen
Wir haben im Beispiel 3.1 S. 3-1 die ggT-Funktion und den ggT-Rechner einfach
zusammen in eine Komponente – je nach Sprache ein Modul oder eine Klasse –
gepackt, aber schon in den Beispielen 3.2 S. 3-17 und 3.3 S. 3-35 die ggT-Klassen und
den ggT-Rechner getrennt in zwei Komponenten realisiert. Nun fordern wir generell
die professionell saubere modulare Trennung zwischen wiederverwendbaren Problemlösungen und ihren Anwendungen oder Testtreibern. Um die beiden Funktionen sorted
und has möglichst wiederverwendbar zu machen, vereinbaren wir sie als Schnittstellenfunktionen einer geeigneten wiederverwendbaren Werkzeugkastenkomponente.
Bild 4.1
Modulares Trennen
verschiedener Aspekte
4.1.2
Anwendung
Testtreiber
Werkzeugkasten
Prüfling
Routinen schachteln oder nicht?
Bei rekursiven Varianten von Routinen stellt sich diese Entwurfsfrage: Die rekursive
Routine muss die sich von Aufruf zu Aufruf ändernden, aber auch die invarianten Größen kennen – woher?
In Sprachen, die wie Component Pascal das Schachteln von Routinen erlauben,
empfiehlt sich, die rekursive Routine in die Schnittstellenroutine zu schachteln und
jede Routine mit den passenden Parametern zu versehen.
In Sprachen, die wie Eiffel und C* kein Schachteln von Routinen erlauben, muss die
Schnittstellenroutine ihre Parameter an die neben ihr stehende rekursive Routine
weiterreichen – entweder über globale Größen oder per Parameterübergabe.
Umformungsschemas zum Entschachteln geschachtelter Routinen beschreibt der
Krimskrams-Beitrag Geschachtelte Routinen entschachteln.
4.1.3
Component Pascal
Werkzeugkasten
Für die Problemlösungen in Component Pascal sehen wir ein Werkzeugkastenmodul
I3ToolsetForArrayOfInteger vor, das wir später erweitern. Der Moduldateiname beginnt
mit Toolset um anzudeuten, dass das Modul neben Sorted und Has weitere Werkzeuge
enthalten kann. Das zu I3ToolsetForArrayOfInteger gehörende Testtreibermodul
I3TesterOfTSAI zeigen wir hier nicht.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
Programm 4.1
Component Pascal:
Sortiertprüfung und
binäres Suchen,
rekursiv
4–3
MODULE I3ToolsetForArrayOfInteger;
IMPORT
BEC := BasisErrorConstants;
TYPE
Integer*
Real*
Numeric*
Comparable*
Any*
= INTEGER;
= REAL;
= Integer;
= Numeric;
= Comparable;
Index*
= INTEGER;
(* Right hand type may be substituted by *)
(* SHORTINT, LONGINT *)
(* SHORTREAL *)
(* Real *)
(* CHAR, ARRAY OF CHAR *)
(* any type *)
PROCEDURE SortedBetween*
(IN array : ARRAY OF Comparable; left, right : Index) : BOOLEAN;
(*!
Postcondition:
result = the elements of array with indices from left to right are ordered.
!*)
BEGIN
ASSERT ((0 <= left) & (right < LEN (array)), BEC.precondParsConsistent);
RETURN
(left >= right) OR
((array [left] <= array [left + 1]) & SortedBetween (array, left + 1, right));
END SortedBetween;
PROCEDURE Sorted* (IN array : ARRAY OF Comparable) : BOOLEAN;
(*!
Postcondition: result = the elements of array are ordered.
!*)
BEGIN
RETURN SortedBetween (array, 0, LEN (array) - 1);
END Sorted;
PROCEDURE Has* (IN array : ARRAY OF Comparable; item : Comparable) : BOOLEAN;
(*!
Postcondition: result = array contains item.
!*)
PROCEDURE HasBetween (left, right : Index) : BOOLEAN;
VAR
mid : Index;
BEGIN
ASSERT ((0 <= left) & (right < LEN (array)), BEC.precondition);
ASSERT (SortedBetween (array, left, right), BEC.precondition);
IF left > right THEN
RETURN FALSE;
ELSE
mid := (left + right) DIV 2;
IF item < array [mid] THEN
RETURN HasBetween (left, mid - 1);
ELSIF item > array [mid] THEN
RETURN HasBetween (mid + 1, right);
ELSE
RETURN TRUE;
END;
END;
END HasBetween;
BEGIN
ASSERT (Sorted (array), BEC.precondition);
RETURN HasBetween (0, LEN (array) - 1);
END Has;
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4–4
4 Reihungen und Abstraktionen
PROCEDURE Sort* (VAR array : ARRAY OF Comparable);
(*!
Effect: Sort the whole array.
!*)
BEGIN
(* To be implemented. *)
ASSERT (Sorted (array), BEC.postcondition);
END Sort;
END I3ToolsetForArrayOfInteger.
Schachteln oder nicht?
Annehmend, dass die Funktion SortedBetween zur partiellen Sortiertprüfung für Kunden interessant sein kann, schachteln wir sie nicht in die Schnittstellenfunktion Sorted
zur vollständigen Sortiertprüfung, sondern veröffentlichen sie. Umgekehrt nehmen wir
bei der partiellen Suchfunktion HasBetween an, dass sie für Kunden uninteressant ist.
Deshalb lassen wir sie geschützt, ja schachteln sie in die Schnittstellenfunktion Has
zum vollständigen Suchen. So braucht HasBetween nur die Suchbereichsgrenzen left
und right als Parameter, da es die Reihung array und das gesuchte Element item in seinem Kontext als Parameter von Has findet.
Iterativ oder rekursiv?
Sowohl SortedBetween als auch HasBetween sind in Programm 4.1 rekursiv implementiert. Auf den ersten Blick scheint SortedBetween nicht endrekursiv zu sein. Ersetzen
wir den Ergebnisausdruck
Partielle
Sortiertprüfung
Endrekursiver Aufruf
(left >= right) OR ((array [left] <= array [left + 1]) & SortedBetween (array, left + 1, right))
mit den kurz ausgewerteten booleschen Operationen durch den äquivalenten mehrfach
bedingten Ausdruck
IF left >= right THEN
TRUE
ELSIF array [left] <= array [left + 1] THEN
SortedBetween (array, left + 1, right)
ELSE
FALSE
END
so erkennen wir die Endrekursion. Die Zweige durch RETURN-Anweisungen ergänzend
kehren wir in die imperative Welt bedingter Anweisungen zurück:
Endrekursiver Aufruf
IF left >= right THEN
RETURN TRUE
ELSIF array [left] <= array [left + 1] THEN
RETURN SortedBetween (array, left + 1, right)
ELSE
RETURN FALSE
END
Auf diesen Algorithmus lässt sich das Schema zur Umformung endrekursiver in iterative Algorithmen von S. 3-5 anwenden. Das Umformungsergebnis vereinfachen und
verschönern wir zur folgenden iterativen Variante der partiellen Sortiertprüfung.
Programm 4.2
Component Pascal:
Partielle
Sortiertprüfung, iterativ
PROCEDURE SortedBetween*
(IN array : ARRAY OF Comparable; left, right : Index) : BOOLEAN;
(*!
Postcondition: result = the elements of array with indices from left to right are ordered.
!*)
BEGIN
ASSERT ((0 <= left) & (right < LEN (array)), BEC.precondParsConsistent);
WHILE (left < right) & (array [left] <= array [left + 1]) DO
INC (left);
END;
RETURN left >= right;
END SortedBetween;
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
Partielles Suchen
4–5
Im Rumpf der Suchfunktion HasBetween in Programm 4.1 erscheint der Erfolgsfall
item = array [mid]
als letzter Fall der mehrfachen Auswahlanweisung, da er bei großem Suchraum statistisch seltener als der Misserfolgsfall vorkommt und eine mehrfache Auswahl ihre
Bedingungen möglichst nach fallender Häufigkeit des Erfülltseins geordnet prüfen soll.
Der Algorithmus von HasBetween in Programm 4.1 mag als strukturiert gelten, da die
Rücksprünge nur in Zweigen stehen, auf die keine Anweisung folgt. Wegen der Rücksprünge lassen sich Zweige einsparen:
Partielles binäres
Suchen, rekursiv,
unstrukturiert
☺
IF left > right THEN
RETURN FALSE;
END;
mid := (left + right) DIV 2;
IF item < array [mid] THEN
RETURN HasBetween (left, mid - 1);
ELSIF item > array [mid] THEN
RETURN HasBetween (mid + 1, right);
END;
RETURN TRUE;
Verglichen mit Programm 3.9 S. 3-4, das einen Zweig nutzlos spart, verbuchen wir hier
als Nutzen die um eins verringerte Schachtelungstiefe. Andererseits verliert die Variante an Struktur, weil der erste Rücksprung drei Anweisungen vor dem statischen Ende
steht. Bei diesen konfligierenden Argumenten zur Verständlichkeit fällt es nicht leicht,
zwischen beiden Varianten des Algorithmus zu entscheiden.
Offenbar sind die rekursiven Varianten der partiellen Suchfunktion endrekursiv, sodass
sich das Schema zur Umformung endrekursiver in iterative Algorithmen von S. 3-5
anwenden lässt. Da die damit erhaltene Bedingungsschleife mindestens einmal durchlaufen wird, wandeln wir sie von der kopfgesteuerten Form in die fußgesteuerte Form
und setzen diese direkt in die vollständige Suchfunktion Has ein.
Programm 4.3
Component Pascal:
Binäres Suchen,
iterativ, unstrukturiert
PROCEDURE Has* (IN array : ARRAY OF Comparable; item : Comparable) : BOOLEAN;
(*!
Postcondition: result = array contains item.
!*)
VAR
left,
right,
mid : Index;
BEGIN
ASSERT (Sorted (array), BEC.precondition);
left := 0;
right := LEN (array) - 1;
REPEAT
mid := (left + right) DIV 2;
IF item < array [mid] THEN
right := mid - 1;
ELSIF item > array [mid] THEN
left := mid + 1;
ELSE
RETURN TRUE;
END;
UNTIL left > right;
RETURN FALSE;
END Has;
Wegen des Rücksprungs aus der Bedingungsschleife heraus ist diese Variante nicht
strukturiert. Wer strukturierte Programme bevorzugt, erhält daraus mit einigen Umformungsschritten die folgende Variante mit kopfgesteuerter Schleife:
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4–6
Binäres Suchen,
iterativ, strukturiert
4 Reihungen und Abstraktionen
left := 0;
right := LEN (array) - 1;
mid := (left + right) DIV 2;
WHILE (left <= right) & (item # array [mid]) DO
IF item < array [mid] THEN
right := mid - 1;
ELSE
left := mid + 1;
END;
mid := (left + right) DIV 2;
END;
RETURN left <= right;
☺
Welche Variante ist besser? Die letzte Variante ist zwar strukturiert, benötigt aber im
Mittel drei Vergleiche pro Iteration, einen weiteren Vergleich nach der Schleife und
berechnet mid einmal umsonst, wobei die Berechnung zweimal hingeschrieben ist. Die
Variante in Programm 4.3 ist dagegen unstrukturiert, benötigt aber im Mittel nur zweieinhalb Vergleiche pro Iteration, keinen Vergleich nach der Schleife und berechnet
nichts umsonst. Hier konfligieren also Effizienz und Verständlichkeit.
4.1.4
Eiffel
4.1.4.1
Werkzeugkasten
Verallgemeinern
In Eiffel, das kein Modulkonstrukt kennt, sehen wir eine Werkzeugkastenklasse
TOOLSET_ARRAY_COMPARABLE vor. Mit Generizität bietet Eiffel eine Möglichkeit,
Lösungen zu verallgemeinern. Generische Klassen lassen sich durch die Variation statischer Typen wiederverwenden, ohne an statischer Typsicherheit zu verlieren. Wir nutzen das Konzept der generischen Klassen, um vom Elementtyp INTEGER zu abstrahieren. Das Problem ist schon mit einem generischen Elementtyp E lösbar, für den eine
Vollordnung definiert ist. Daher genügt es zu fordern, dass E die Vollordnung von der
Standardbibliotheksklasse COMPARABLE erbt:
E -> COMPARABLE
COMPARABLE
COMPARABLE definiert die Relationen =, /=, <, <=, >, >= in üblicher Infixnotation teilweise abstrakt, teilweise implementiert. Die Definition der generischen Werkzeugkastenklasse beginnt mit
class TOOLSET_ARRAY_COMPARABLE [E -> COMPARABLE]
Bei der Vereinbarung einer Größe dieser Klasse ist der formale generische Parameter E
zu konkretisieren, etwa so:
tsac : TOOLSET_ARRAY_COMPARABLE [INTEGER]
Für E als aktuellen generischen Parameter INTEGER eingesetzt liefert die verlangte spezielle Lösung für ganzzahlige Reihungen.
Eingeschränkte und
uneingeschränkte
Generizität
Da der generische Parameter die Vererbungsbeziehung erfüllen muss, spricht man von
eingeschränkter Generizität (restricted genericity). Dagegen bedeutet uneingeschränkte Generizität (unrestricted genericity), dass an generische Parameter keine
expliziten Bedingungen gestellt werden können.
Eingeschränkte Generizität ist ausdrucksfähiger als uneingeschränkte Generizität und
erlaubt die Prüfung explizit formulierter Einschränkungen an der Stelle der Definition
einer generischen Einheit. Uneingeschränkte Generizität ist flexibler einsetzbar als eingeschränkte Generizität, aber die implizit dennoch vorhandenen Einschränkungen lassen sich erst bei der Benutzung einer generischen Einheit prüfen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
Programm 4.4
Eiffel: Sortiertprüfung
iterativ; binäres
Suchen, rekursiv
4–7
note
description: "Tools operating on arrays of comparable elements"
class TOOLSET_ARRAY_COMPARABLE [E -> COMPARABLE]
feature
sorted_between (array : ARRAY [E]; left, right : INTEGER) : BOOLEAN
-- Are the elements of array with indices from left to right ordered?
require
array_exists:
array /= Void
left_large_enough:
array.lower <= left
right_small_enough: right <= array.upper
local
index : INTEGER
do
from
index := left
until index >= right or else array.item (index) > array.item (index + 1) loop
index := index + 1
end
Result := index >= right
end -- sorted_between
sorted (array : ARRAY [E]) : BOOLEAN
-- Are the elements of array ordered?
require
array_exists: array /= Void
do
Result := sorted_between (array, array.lower, array.upper)
end -- sorted
feature {NONE}
has_between (array : ARRAY [E]; item : E; left, right : INTEGER) : BOOLEAN
-- Does array contain item between positions left and right?
require
array_exists:
array /= Void
left_large_enough:
array.lower <= left
right_small_enough: right <= array.upper
sorted:
sorted_between (array, left, right)
local
mid : INTEGER
do
if left <= right then
mid := (left + right) // 2
if item < array.item (mid) then
Result := has_between (array, item, left, mid - 1)
elseif item > array.item (mid) then
Result := has_between (array, item, mid + 1, right)
else
Result := True
end
end
end -- has_between
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4–8
4 Reihungen und Abstraktionen
feature
has (array : ARRAY [E]; item : E) : BOOLEAN
-- Does array contain item?
require
array_exists: array /= Void
sorted:
sorted (array)
do
Result := has_between (array, item, array.lower, array.upper)
end -- has
sort (array : ARRAY [E])
-- Sort the elements of array in ascending order.
require
array_exists: array /= Void
do
-- To be implemented.
ensure
sorted: sorted (array)
end -- sort
end -- class TOOLSET_ARRAY_COMPARABLE
Formal und aktuell
In der Signatur
sorted_between (array : ARRAY [E]; left, right : INTEGER) : BOOLEAN
ist der formale Parameter array eine Referenz auf ein Objekt des Typs ARRAY [E]. Die
Parameterübergabeart entspricht der Eingabe-Referenzübergabe (IN-Parameter in Component Pascal). Als aktueller generischer Parameter der generischen Klasse ARRAY
wird hier der formale generische Parameter E der generischen Klasse
TOOLSET_ARRAY_COMPARABLE eingesetzt.
Implizites Initialisieren
Eiffel initialisiert alle Attribute und lokalen Variablen implizit mit Standardwerten:
Zahlen mit Nullen, boolesche Größen mit False. Deshalb kann has_between einen Zweig
sparen.
Strukturiertes
Programmieren
Während Algorithmen in Component Pascal und C* nicht den strengen Anforderungen
strukturierten Programmierens entsprechen müssen, da sie wegen möglicher Rücksprünge aus Alternativen und Schleifen mehrere Ausgänge haben können, erfüllen alle
Eiffel-Algorithmen die 1-Ein-/1-Ausgang-Regel. In Eiffel ist es unmöglich, unstrukturiert zu programmieren!
Aufgabe 4.1
Eiffel: Sortiertprüfung
rekursiv, binäres
Suchen iterativ
Implementieren Sie eine
(1)
(2)
rekursive Eiffel-Variante der Sortiertprüfungsfunktion sorted_between,
iterative Eiffel-Variante der Suchfunktion has
als Teil der Klasse TOOLSET_ARRAY_COMPARABLE! Zeigen Sie jeweils, dass sich die
rekursive Variante nach dem Umformungsschema von S. 3-5 in die iterative Varianten
überführen lässt!
4.1.4.2
Testtreiber
Um das binäre Suchen der Werkzeugkastenklasse TOOLSET_ARRAY_COMPARABLE testen zu können, schreiben wir zunächst eine einfache Testtreiberklasse
TESTER_OF_TSACI_HAS. Später werden wir diesen Testtreiber verallgemeinern und
wiederverwendbare Teile herausfaktorisieren. Methodisch gesehen realisiert der Testtreiber einen randomisierten Einzeltest. Damit lassen sich schnell viele verschiedene
Testfälle abdecken. Ein Testfall besteht aus einer ganzzahligen Reihung beliebiger
Länge und einer gesuchten Ganzzahl. Statt Testfälle manuell einzugeben, lassen wir sie
im Testtreiber von einem Zufallszahlengenerator erzeugen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
Zufallszahlengenerator
Einmalfunktion
4–9
RANDOM ist eine Bibliotheksklasse für Zufallszahlen. Sie funktioniert nach dem in Eif-
fel üblichen Kommando-Abfrage-Schema, da in Eiffel Nebeneffekte verpönt sind. Entsprechend definieren wir random_item als nebeneffektfreie Funktion. Das benötigte
RANDOM-Objekt wird von der Einmalfunktion (siehe S. 3-22) random nur beim ersten
Aufruf erzeugt, alle folgenden Aufrufe von random liefern eine Referenz auf dasselbe
Objekt.
Geschützte
Wurzelprozedur
Erzeugungsprozeduren können auch durch feature {NONE} geschützt sein. Sie sind
dann nur aus Nachfolgerklassen und als Wurzelprozeduren aufrufbar.
Ausgabe
Die Reihung wird mit der von ANY geerbten print-Prozedur ausgegeben. Wirkt sie nicht
wie gewünscht, so sollte man statt print das von ANY geerbte Merkmal
out : STRING
redefinieren, welches das Objekt druckbar darstellt.
Programm 4.5
Eiffel: Testtreiber zu
binärem Suchen
note
description: "Test driver for type TOOLSET_ARRAY_COMPARABLE [INTEGER]"
class TESTER_OF_TSACI_HAS
create
make
feature {NONE}
Min : INTEGER = -1000
Max : INTEGER = 1000
random : RANDOM
once
create Result.make
ensure
random_exists: Result /= Void
end -- random
random_item : INTEGER
require
random_exists: random /= Void
do
Result := (((random.item - 1) / (random.modulus - 2)) * (Max - Min) + Min).floor
ensure
large_enough: Min <= Result
small_enough: Result <= Max
end -- random_item
init_random (array : ARRAY [INTEGER])
require
random_exists: random /= Void
array_exists:
array /= Void
local
index : INTEGER
do
from
index := array.lower
until index > array.upper loop
random.forth
array.put (random_item, index)
index := index + 1
end
end -- init_random
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 10
4 Reihungen und Abstraktionen
make
-- Given a positive integer command argument, fill an integer array of given
-- size with random values and check if it contains another random value.
local
command_line : ARGUMENTS
tsaci
: TOOLSET_ARRAY_COMPARABLE [INTEGER]
array
: ARRAY [INTEGER]
item
: INTEGER
do
create command_line
if command_line.argument_count < 1 or else
not command_line.argument (1).is_integer or else
command_line.argument (1).to_integer <= 0
then
io.put_string ("Please call this command with a positive integer argument" +
" for the array size!")
else
create tsaci
create array.make (1, command_line.argument (1).to_integer)
init_random (array)
tsaci.sort (array)
random.forth
item := random_item
io.put_string ("array: ");
print (array)
io.put_string ("item ");
io.put_integer (item)
if not tsaci.has (array, item) then
io.put_string (" not")
end
io.put_string (" found in array.")
end
io.put_new_line
end -- make
end -- class TESTER_OF_TSACI_HAS
4.1.5
C++
C++ kennt zwar kein Modulkonstrukt, bietet aber zwei Möglichkeiten, ein Werkzeugkastenmodul zu realisieren, die wir beide vorstellen: Namensräume und Klassenfunktionen. Auch für die generische Verallgemeinerung gibt es zwei Möglichkeiten, die wir
beide vorstellen: generische Funktionen und generische Klassen, der hybriden prozedural-objektorientierten Konzeption von C++ entsprechend. Im Unterschied zu Eiffel
kennt C++ nur uneingeschränkte Generizität; die Bezeichnung dafür ist Schablone
(template).
4.1.5.1
Werkzeugkasten: Namensraum und generische Funktionen
Die erste C++-Variante verbindet das prozedural-modulare Muster der ComponentPascal-Variante mit der generischen Verallgemeinerung der Eiffel-Variante. Die
Module von Component Pascal lassen sich in C++ mit Namensräumen nachbilden.
Modulnamen in Component Pascal setzen sich aus zwei Teilen zusammen, z.B.
I3ToolsetForArrayOfInteger
aus dem Subsystemnamen I3 und dem Moduldateinamen ToolsetForArrayOfInteger. Nur
der Subsystemname muss systemweit eindeutig sein. Der Zugriff erfolgt mit der Punktnotation:
b := I3ToolsetForArrayOfInteger.Sorted (array);
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
4 – 11
Die zweistufige Namensgebung lässt sich in C++ durch zwei geschachtelte Namensräume realisieren:
Geschachtelte
Namensräume
namespace I3 {
namespace ToolsetArrayInteger {
bool sorted (int array []);
} // end of ToolsetArrayInteger
} // end of I3
Entsprechend wird zweistufig qualifiziert mit dem Gültigkeitsbereichsauflösungsoperator zugegriffen:
b = I3::ToolsetArrayInteger::sorted(array);
Eiffel bietet keine mehrstufigen Namensräume. Bei der Eiffel-Variante muss der Klassenname
TOOLSET_ARRAY_COMPARABLE
systemweit eindeutig sein. Deshalb begnügen wir uns der Einfachheit halber auch bei
der C++-Variante mit einstufiger Namensgebung:
Einfacher
Namensraum
namespace ToolsetArrayInteger {
bool sorted (int array []);
} // end of ToolsetArrayInteger
Es wird einstufig qualifiziert zugegriffen:
b = ToolsetArrayInteger::sorted(array);
Kapseln und schützen
Component-Pascal-Module und C++-Namensräume unterscheiden sich darin, dass
Module auch Schutzeinheiten sind, die ihre Merkmale vor externen Zugriffen schützen
können, während Namensräume Merkmale nur kapseln, nicht schützen.
Speichern und
übersetzen
Während in Component Pascal Module, Quelltextdateien und Übersetzungseinheiten
sich eineindeutig entsprechen, stehen in C++ Namensräume, Quelltextdateien und
Übersetzungseinheiten orthogonal zueinander. Beispielsweise kann sich ein Namensraum über mehrere Quelltextdateien erstrecken, eine Quelltextdatei kann Teile mehrerer Namensräume enthalten. Der Programmierer kann die Beziehungen zwischen diesen Einheiten flexibel gestalten, das Ergebnis kann aber unübersichtlich werden.
Deshalb legen wir mit einer an Component Pascal orientierten Konvention fest, wie
wir Module in den C++-Beispielprogrammen dieses Skripts realisieren:
Realisierung von
Modulen
Zu einem Modul gehören höchstens eine Schnittstellendatei und höchstens eine
Implementationsdatei, mindestens aber eine von beiden.
Alle Teile eines Moduls gehören zu genau einem Namensraum, der nur dieses
Modul umfasst.
Jede Quelltextdatei enthält höchstens ein Modul und einen zugehörigen Namensraum.
Die von C geerbten Reihungen, vereinbart z.B. durch
C-Reihung
int array [];
sind unsicher, da zur Laufzeit nicht prüfbar ist, ob ein Index innerhalb der zulässigen
Grenzen liegt. Deshalb hält sich der professionelle Programmierer an die von Stroustrup empfohlene Programmierleitlinie [Str97], S. 14:
Leitlinie 4.1
C++: Benutze Vektoren
statt Reihungen
27.9.12
Vermeide, die unsicheren C-Reihungen zu verwenden! Verwende stattdessen die
sichere STL-Klasse vector!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 12
4 Reihungen und Abstraktionen
STL ist die Abkürzung für die Standard Template Library, die zum ANSI/ISO-Standard
von C++ gehört. Mit der generischen Klasse vector, zu deren Benutzung ihre Schnittstellendatei durch
#include <vector>
zu inkludieren ist, ist
C++-Vektor
std::vector<int> array;
eine entsprechende Vereinbarung.
Programm 4.6
C++: Sortiertprüfung,
iterativ; binäres
Suchen, rekursiv;
Schnittstelle mit
generischen
Funktionen
// Header file: ToolsetArrayComparableM.hpp
#ifndef TOOLSET_ARRAY_COMPARABLE_M_HPP_
#define TOOLSET_ARRAY_COMPARABLE_M_HPP_
#include <cassert>
#include <vector>
namespace ToolsetArrayComparable {
template <typename T>
bool sorted_between (const std::vector<T> & array, int left, int right)
// Instantiation condition: T is a comparable type.
{
assert(left >= 0);
// precondition
assert(right < array.size()); // precondition
while (left < right && array[left] <= array[left + 1]) {
++left;
}
return left >= right;
}
template <typename T>
bool sorted (const std::vector<T> & array)
// Instantiation condition: T is a comparable type.
{
return sorted_between(array, 0, array.size() - 1);
}
template <typename T>
bool has_between (const std::vector<T> & array, const T & item, int left, int right)
// Instantiation condition: T is a comparable type.
{
assert(left >= 0);
// precondition
assert(right < array.size());
// precondition
assert(sorted_between(array, left, right)); // precondition
if (left > right) {
return false;
} else {
int mid = (left + right) / 2;
if (item < array[mid]) {
return has_between(array, item, left, mid - 1);
} else if (item > array[mid]) {
return has_between(array, item, mid + 1, right);
} else {
return true;
}
}
}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
4 – 13
template <typename T>
bool has (const std::vector<T> & array, const T & item)
// Instantiation condition: T is a comparable type.
{
assert(sorted(array)); // precondition
return has_between(array, item, 0, array.size() - 1);
}
template <typename T>
void sort (std::vector<T> & array)
// Instantiation condition: T is a comparable type.
{
// To be implemented.
assert(sorted(array)); // postcondition
}
} // ToolsetArrayComparable
#endif // TOOLSET_ARRAY_COMPARABLE_M_HPP_
Generische Funktion
template <typename T> bool sorted (const std::vector<T> & array)
deklariert die generische Funktion sorted mit dem formalen generischen Parameter T.
Statt
template <typename T>
könnten wir
template <class T>
schreiben, doch da wir T durch Basistypen wie int und float konkretisieren wollen, die
keine Klassen sind, passt typename besser als class. In
bool sorted (const std::vector<T> & array)
ist der formale Parameter array eine Referenz (&) auf ein konstantes Objekt (const)
vom Typ std::vector<T>. Die Parameterübergabeart entspricht der Eingabe-Referenzübergabe (IN-Parameter in Component Pascal). Die generische Klasse vector gehört zum
Namensraum std; als aktueller generischer Parameter für vector wird hier der formale
generische Parameter T von sorted eingesetzt. Einschränkungen an T sind nicht ausdrückbar. Erst die Implementation von sorted offenbart, dass für T die Vergleichsoperatoren definiert sein müssen. Der Kommentar
// Instantiation condition: T is a comparable type.
soll Anwender vorab darauf aufmerksam machen.
Implementation in der
Schnittstelle
Normalerweise enthält eine Schnittstellendatei nur die Deklarationen der exportierten
Funktionen. Doch bei generischen Funktionen (Klassen) muss die Schnittstellendatei
auch die Implementationen der Funktionen (Klassen) vollständig enthalten, denn nur so
kann der Kompilierer an inkludierenden Stellen Code für Konkretisierungen der Funktionen (Klassen) erzeugen – eine Folge der uneingeschränkten Generizität. Eine Konsequenz davon ist, dass die in der Eiffel-Variante versteckt gehaltene generische Funktion
has_between in dieser C++-Variante mit ihrer Implementation in die Schnittstellendatei
aufgenommen werden muss und damit öffentlich ist.
Indexbereich
Der Indexbereich von vector-Objekten beginnt wie der von C-Reihungen bei 0. Im
Unterschied zu C-Reihungen ist bei vector-Objekten die Anzahl der Elemente dynamisch mit der size()-Funktion abfragbar.
Mehrseitige Auswahl
Die C*-Sprachen kennen nur die zweiseitige if-else-Auswahlanweisung. Mehrseitige
Auswahlen sind durch geschachtelte if-else zu realisieren. Statt bei jedem geschachtelten if-else tiefer einzurücken:
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 14
4 Reihungen und Abstraktionen
if (...) {
...
} else {
if (...) {
...
} else {
if (...) {
...
} else {
...
}
}
}
empfiehlt sich, die Konvention des Blockens der Zweige zu lockern und nach else
geschachtelte if-else-Anweisungen nicht zu klammern:
☺
Aufgabe 4.2
C++: Sortiertprüfung
rekursiv, binäres
Suchen iterativ
if (...) {
...
} else if (...) {
...
} else if (...) {
...
} else {
...
}
Implementieren Sie eine
(1)
rekursive
C++-Variante
der
generischen
Sortiertprüfungsfunktion
sorted_between,
(2)
iterative C++-Variante der generischen Suchfunktion has
als Teil der Schnittstellendatei ToolsetArrayComparableM.hpp! Zeigen Sie jeweils, dass
sich die rekursive Variante nach dem Umformungsschema von S. 3-5 in die iterative
Varianten überführen lässt!
4.1.5.2
Werkzeugkasten: Generische Klasse und Klassenfunktionen
Eine alternative Realisierung des Werkzeugkastenmoduls verwendet anstelle der generischen Funktionen ähnlich wie die Eiffel-Variante eine generische Klasse. Die Deklaration der generischen Werkzeugkastenklasse beginnt mit
Generische Klasse
template <typename T> class ToolsetArrayComparable
Da diese Klasse nur Werkzeuge bietet und keinen Zustand hat, genügt von ihr ein
Objekt. Die Situation entspricht dem Entwurfsmuster Singleton. Während Eiffel Singletons und Module nicht sehr gut unterstützt, bietet C++ dafür Klassenfunktionen,
also Funktionen, die durch den Spezifikator static an die Klasse gebunden und sich mit
dem Klassennamen qualifiziert aufrufen lassen. Bei generischen Klassen sind die formalen generischen Parameter zu konkretisieren, hier etwa so:
ToolsetArrayComparable<int>::sorted(array)
Für T als aktuellen generischen Parameter int eingesetzt liefert die verlangte spezielle
Lösung für ganzzahlige Reihungen. Da die Klasse einen eigenen Namensraum definiert, verzichten wir bei dieser Variante auf einen zusätzlichen Namensraum.
template <typename T> class ToolsetArrayComparable {
...
}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
Programm 4.7
C++: Sortiertprüfung,
iterativ; binäres
Suchen, rekursiv;
Schnittstelle mit
generischer Klasse
4 – 15
// Header file: ToolsetArrayComparableC.hpp
#ifndef TOOLSET_ARRAY_COMPARABLE_C_HPP_
#define TOOLSET_ARRAY_COMPARABLE_C_HPP_
#include <cassert>
#include <vector>
template <typename T>
class ToolsetArrayComparable
// Instantiation condition: T is a comparable type.
{
public:
static bool sorted_between (const std::vector<T> & array, int left, int right);
static bool sorted (const std::vector<T> & array);
static bool has (const std::vector<T> & array, const T & item);
static void sort (std::vector<T> & array);
protected:
static bool has_between
(const std::vector<T> & array, const T & item, int left, int right);
}; // ToolsetArrayComparable
template <typename T>
bool ToolsetArrayComparable<T>::sorted_between
(const std::vector<T> & array, int left, int right)
{
assert(left >= 0);
// precondition
assert(right < array.size()); // precondition
while (left < right && array[left] <= array[left + 1]) {
++left;
}
return left >= right;
}
template <typename T>
bool ToolsetArrayComparable<T>::sorted (const std::vector<T> & array) {
return sorted_between(0, array.size() - 1);
}
template <typename T>
bool ToolsetArrayComparable<T>::has_between
(const std::vector<T> & array, const T item, int left, int right) {
assert(left >= 0);
// precondition
assert(right < array.size());
// precondition
assert(sorted_between(array, left, right)); // precondition
if (left > right) {
return false;
} else {
int mid = (left + right) / 2;
if (item < array[mid]) {
return has_between(array, item, left, mid - 1);
} else if (item > array[mid]) {
return has_between(array, item, mid + 1, right);
} else {
return true;
}
}
}
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 16
4 Reihungen und Abstraktionen
template <typename T>
bool ToolsetArrayComparable<T>::has (const std::vector<T> & array, const T item) {
assert(sorted(array)); // precondition
return has_between(array, item, 0, array.size() - 1);
}
template <typename T>
void ToolsetArrayComparable<T>::sort (std::vector<T> & array) {
// To be implemented.
assert(sorted(array)); // postcondition
}
#endif // TOOLSET_ARRAY_COMPARABLE_C_HPP_
deklariert die generische Klasse ToolsetArrayComparable mit dem formalen generischen
Parameter T. Zwischen den Klassenklammern steht nur die Schnittstelle der Klasse.
Implementationen von Klassen- und Elementfunktionen stehen außerhalb davon, normalerweise in der zugehörigen Implementationsdatei. Da es sich hier jedoch um eine
generische Klasse handelt, müssen aus dem oben genannten Grund alle Implementationen in der Schnittstellendatei enthalten sein.
Aufgabe 4.3
C++: Sortiertprüfung
rekursiv, binäres
Suchen iterativ
Implementieren Sie eine
(1)
(2)
rekursive C++-Variante der Sortiertprüfungs-Klassenfunktion sorted_between,
iterative C++-Variante der Such-Klassenfunktion has
als Teil der Schnittstellendatei ToolsetArrayComparableC.hpp! Zeigen Sie jeweils, dass
sich die rekursive Variante nach dem Umformungsschema von S. 3-5 in die iterative
Varianten überführen lässt!
4.1.5.3
Testtreiber
Um die Schnittstellendateien ToolsetArrayComparableM.hpp und ToolsetArrayComparableC.hpp testen zu können, schreiben wir analog zur Eiffel-Variante zunächst einfache
Testtreibermodule TesterOfTSACIMHas und TesterOfTSACICHas, die später zu verallgemeinern sind.
Programm 4.8
C++: Testtreiber zu
binärem Suchen mit
generischen
Funktionen
// Implementation file: TesterOfTSACIMHas.cpp
#include <cstdlib>
#include <iostream>
#include "ToolsetArrayComparableM.hpp"
typedef long Element;
static const Element min = -1000;
static const Element max = 1000;
static Element random_item () {
Element result =
Element((double(std::rand() - 1) / (RAND_MAX - 2)) * (max - min) + min);
assert(min <= result); // postcondition
assert(result <= max); // postcondition
return result;
}
static void init_random (std::vector<Element> & array) {
for (int index = 0; index < array.size(); ++index) {
array[index] = random_item();
}
}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
4 – 17
static std::ostream& operator<< (std::ostream& s, const std::vector<Element> & array)
{
s << "[";
for (int index = 0; index < array.size(); ++index) {
s << array[index] << " ";
}
s << "]" << std::endl;
}
int main (int argc, char * argv[]) {
if (argc < 2 || std::atoi(argv[1]) <= 0) {
std::cout << "Please call this command with a positive integer argument"
+ " for the array size!";
} else {
std::vector<Element> array (std::atoi(argv[1]));
init_random(array);
ToolsetArrayComparable::sort(array);
Element item = random_item();
std::cout << "array: " << array << "item " << item
<< ((ToolsetArrayComparable::has(array, item)) ? "" : " not")
<< " found in array.";
}
std::cout << std::endl;
return 0;
}
Zufallszahlengenerator
rand ist eine nebeneffektbehaftete Bibliotheksfunktion für Zufallszahlen. Entsprechend
definieren wir random_item als nebeneffektbehaftete Funktion.
Ausgabe
Um die Reihung auszugeben, ist eine Prozedur zu definieren. Das C++-typische Idiom
dafür ist, den <<-Operator zu überladen.
Der Testtreiber für die Klassenvariante unterscheidet sich vom Testtreiber für die
Modulvariante nur an wenigen Stellen.
Programm 4.9
C++: Testtreiber zu
binärem Suchen mit
generischer Klasse
// Implementation file: TesterOfTSACICHas.cpp
...
#include "ToolsetArrayComparableC.hpp"
...
ToolsetArrayComparable<Element>::sort(array);
...
<< ((ToolsetArrayComparable<Element>::has(array, item)) ? "" : " not")
...
4.1.6
Java
4.1.6.1
Werkzeugkasten
Ursprünglich ohne Generizität entworfen, unterstützt Java seit Release 5.0 das von Eiffel und C++ bekannte Konzept in Form generischer Klassen und Schnittstellen; die
Bezeichnung dafür ist auch parametrisierter Typ (parameterized type). Wie Eiffel
erlaubt Java eingeschränkte Generizität; generische Typen sind durch eine Klasse und
beliebig viele Schnittstellen einschränkbar. Entsprechend stellen wir eine generische
Werkzeugkastenklasse ToolsetArrayComparable vor. Für den generischen Elementtyp E fordern wir, dass er die generische Schnittstelle Comparable<T> aus dem Java
Collections Framework implementiert:
E extends Comparable<E>
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 18
4 Reihungen und Abstraktionen
(Java verwendet das Schlüsselwort implements für das Implementieren von Schnittstellen, trotzdem muss hier extends statt implements stehen.) E erscheint sowohl als
formaler als auch als aktueller generischer Parameter, damit zwei Elemente gleichen
Typs vergleichbar werden. Dagegen braucht die Eiffel-Klasse COMPARABLE nicht
generisch zu sein, da Eiffel das Problem anders – mit verankerten Typen – löst.
Comparable
Die Schnittstelle Comparable<T> hat nur eine einzige Methode mit der Signatur
int compareTo (T o)
und der Semantik, einen negativen oder positiven Wert oder 0 zu liefern, wenn das
this-Objekt (links) kleiner, größer bzw. gleich dem o-Objekt (rechts) ist. Dem Vorteil
der schlanken Schnittstelle steht der Nachteil unhandlicher Vergleiche entgegen. Die
Definition der generischen Werkzeugkastenklasse beginnt mit
public class ToolsetArrayComparable<E extends Comparable<E>>
fungiert nicht wie bei der C++-Variante Programm 4.7 als
Modul, da Klassenfunktionen anscheinend nicht mit generischen Typen arbeiten können. Bei der Vereinbarung eines Referenzobjekts dieser Klasse ist der formale generische Parameter E zu konkretisieren, etwa so:
ToolsetArrayComparable
ToolsetArrayComparable<Integer> tsaci =
new ToolsetArrayComparable<Integer>();
Für E als aktuellen generischen Parameter die Hüllenklasse Integer eingesetzt liefert
die verlangte spezielle Lösung für ganzzahlige Reihungen.
Programm 4.10
Java: Sortiertprüfung,
iterativ; binäres
Suchen, rekursiv
public class ToolsetArrayComparable<E extends Comparable<E>> {
public boolean sortedBetween (E[] array, int left, int right) {
assert array != null
: "precondition";
assert left >= 0
: "precondition";
assert right < array.length : "precondition";
while (left < right &&
array[left].compareTo(array[left + 1]) <= 0)
{
++left;
}
return left >= right;
}
public boolean sorted (E[] array) {
assert array != null : "precondition";
return sortedBetween(array, 0, array.length - 1);
}
private boolean hasBetween
(E[] array, E item, int left, int right) {
assert array != null
: "precondition";
assert left >= 0
: "precondition";
assert right < array.length
: "precondition";
assert sortedBetween(array, left, right) : "precondition";
if (left > right) {
return false;
} else {
int mid = (left + right) / 2;
if (item.compareTo(array[mid]) < 0) {
return hasBetween(array, item, left, mid - 1);
} else if (item.compareTo(array[mid]) > 0) {
return hasBetween(array, item, mid + 1, right);
} else {
return true;
}
}
}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
4 – 19
public boolean has (E[] array, E item) {
assert array != null : "precondition";
assert sorted(array) : "precondition";
return hasBetween(array, item, 0, array.length - 1);
}
public void sort (E[] array) {
assert array != null : "precondition";
// To be implemented.
assert sorted(array) : "postcondition";
}
}
Aufgabe 4.4
Java: Sortiertprüfung
rekursiv, binäres
Suchen iterativ
Implementieren Sie eine
(1)
(2)
rekursive Java-Variante der Sortiertprüfungsfunktion sortedBetween,
iterative Java-Variante der Suchfunktion has
als Teil der Klasse ToolsetArrayInteger! Zeigen Sie jeweils, dass sich die rekursive
Variante nach dem Umformungsschema von S. 3-5 in die iterative Varianten überführen lässt!
4.1.6.2
Testtreiber
Zum Testen der Werkzeugkastenklasse ToolsetArrayInteger schreiben wir analog zu
den Eiffel- und C++-Varianten eine einfache Testtreiberklasse TesterOfTSACIHas.
Zufallszahlengenerator
Random ist eine Klasse für Zufallszahlen mit einer nebeneffektbehafteten Funktion
nextInt. Entsprechend definieren wir randomItem als nebeneffektbehaftete Funktion.
Ausgabe
Um die Reihung auszugeben, ist eine normale Prozedur zu definieren.
Programm 4.11
Java: Testtreiber zu
binärem Suchen
public class TesterOfTSACIHas {
private static final int MIN = -1000;
private static final int MAX = 1000;
private static java.util.Random random = new java.util.Random();
private static int randomItem () {
assert random != null : "precondition";
int result = random.nextInt(MAX - MIN + 1) - MIN;
assert MIN <= result : "postcondition";
assert result <= MAX : "postcondition";
return result;
}
private static void initRandom (Integer[] array) {
assert random != null : "precondition";
assert array != null
: "precondition";
for (int index = 0; index < array.length; ++index) {
array[index] = randomItem();
}
}
private static void print (Integer[] array) {
assert array != null : "precondition";
System.out.print("[");
for (int index = 0; index < array.length; ++index) {
System.out.print(array[index] + " ");
}
System.out.println("]");
}
private static final String HINT =
"Please call this command with a positive integer argument"
+ " for the array size!";
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 20
4 Reihungen und Abstraktionen
public static void main (String[] args) {
if (args.length < 1) {
System.out.println (HINT);
return;
}
int size = 0;
try {
size = Integer.parseInt(args[0]);
} catch (Exception e) {
System.out.println (HINT);
return;
}
if (size <= 0) {
System.out.println (HINT);
return;
}
ToolsetArrayComparable<Integer> tsaci =
new ToolsetArrayComparable<Integer>();
Integer[] array = new Integer[size];
initRandom(array);
tsaci.sort(array);
int item = randomItem();
System.out.print("array: ");
print(array);
System.out.print("item " + item);
if (!ToolsetArrayInteger.has(array, item)) {
System.out.print(" not");
}
System.out.print(" found in array.");
}
}
Erlaubte Typarten
Java erlaubt als Typarten für aktuelle generische Parameter nur Klassen, keine primitiven Typen, da diese keine Klassen sind. Statt primitive Typen muss man ihre Hüllenklassen nehmen (int → Integer). Automatisches Ver-/Enthüllen (autoboxing) soll die
Handhabung vereinfachen.
Eiffel und C++ erlauben alle Typarten für aktuelle generische Parameter; Eiffel wegen
seines homogenen Typsystems, bei dem alle Klassen von der allgemeinen Klasse ANY
erben; C++ wegen der Makroexpansion als Übersetzungstechnik. Java bleibt einerseits mit seinem heterogenen Typsystem näher bei C++ und kann nicht wie Eiffel die
Vorteile eines homogenen Typsystems nutzen. Andererseits verzichtet Java auf die primitive, aber flexible Technik der Makroexpansion von C++.
4.1.7
C#
Wie Java ursprünglich ohne Generizität entworfen, unterstützt C# ab dem .NET 2.0
Framework generische Klassen und Schnittstellen. Wie Eiffel und Java erlaubt C# eingeschränkte Generizität; generische Typen sind durch eine Klasse und beliebig viele
Schnittstellen einschränkbar. Entsprechend ist auch in C# eine generische Werkzeugkastenklasse ToolsetArrayComparable zu realisieren. Für den generischen Elementtyp E fordern wir, dass er die generische Schnittstelle IComparable<T> aus der .NET
Framework Class Library implementiert:
public class ToolsetArrayComparable<E> where E : IComparable<E>
Aufgabe 4.5
C#: Sortiertprüfung,
binäres Suchen
Entwickeln Sie rekursive und iterative C#-Varianten zu den Problemen von 4.1! Entwickeln Sie auch einen Testtreiber dazu!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.1 Binäres Suchen in einer sortierten Reihung
4.1.8
4 – 21
Fazit
Sortiertprüfung und binäres Suchen sind funktionale Probleme, die sich mit rekursiven
und iterativen Algorithmen lösen lassen. Die Implementationen der Algorithmen in den
Auswahlsprachen unterscheiden sich nur unwesentlich. Die Problemlösungen werden
in einer Werkzeugkastenkomponente zusammengefasst.
Bei den generischen Verallgemeinerungen der Werkzeugkastenkomponenten zeigen
sich Gemeinsamkeiten und Unterschiede:
In Eiffel, C++, Java und C# lässt sich die Werkzeugkastenkomponente als generische Klasse realisieren.
Eiffel, Java und C# erlauben eingeschränkte Generizität; damit lässt sich die Einschränkung, dass für den Elementtyp eine Vollordnung definiert ist, explizit spezifizieren. In C++ ist diese Einschränkung implizit in der Implementation der generischen Klasse versteckt.
Eiffel, C++ und C# erlauben beliebige Typen als aktuelle generische Parameter,
sofern sie die Einschränkungen erfüllen. Java erlaubt nur Klassen als aktuelle generische Parameter, sodass statt primitiver Typen ihre Hüllenklassen einzusetzen sind.
Unterstützt die Sprache auch generische Module?
Eiffel ermöglicht generische Module nur wie allgemeine Module über Einmalfunktionen, während die C*-Sprachen Module durch Klassenfunktionen ermöglichen.
C++ erlaubt generische Klassen mit Klassenfunktionen, die mit generischen
Parametern arbeiten, sodass solche Klassen als generische Module fungieren
können.
Javas Klassenfunktionen können nicht mit generischen Parametern arbeiten,
sodass sich generische Module nur ähnlich wie in Eiffel realisieren lassen.
C# erlaubt generische Klassenfunktionen, sodass Klassen mit solchen generischen Klassenfunktionen als generische Module fungieren können.
C++ kennt auch generische Funktionen und ermöglicht zusammen mit Namensräumen eine Realisierung, die einem generischen Modul nahe kommt.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 22
4.2
4 Reihungen und Abstraktionen
Sortieren einer Reihung mit Quicksort
Sortieren von Daten ist eine oft vorkommende Aufgabe in der Informatik.
Problem Sortieren
Gegeben:
Eine Reihung array von Elementen mit einer Vollordnungsrelation ≤.
Gesucht:
Die Reihung mit denselben Elementen, aber aufsteigend geordnet.
Mathematisch lässt sich das Problem als Abbildung auffassen, etwa
sort : Gn → Gn.
Die passende imperative Lösung ist eine Prozedur mit der Reihung als Ein-/AusgabeParameter, implementiert mit einem rekursiven oder iterativen Algorithmus. Wir ordnen Sortierprozeduren der Werkzeugkastenkomponente von 4.1 zu.
Quicksort
Quicksort ist ein von C. A. R. Hoare 1962 erfundener beliebter effizienter Sortieralgorithmus, der sich gut rekursiv formulieren lässt und einen Laufzeitaufwand der Größenordnung O(n * log(n)) hat. Da Quicksort in zahllosen Lehrbüchern beschrieben ist,
genügt hier eine kurze Beschreibung. Quicksort nutzt das Teile-und-herrsche-Prinzip:
Wähle ein Element der Reihung als Trennelement, bringe alle kleineren Elemente nach
links und alle größeren Elemente nach rechts. Behandle die Reihungsteile links und
rechts des Trennelements ebenso.
Viele Algorithmen lassen sich mit Listenkonstruktoren kompakt formulieren. Als Beispiel geben wir eine funktionale rekursive Implementation von Quicksort in der funktionalen Programmiersprache Miranda an, die als Spezifikation dienen soll.
Programm 4.12
Miranda: Quicksort
quicksort [ ] = [ ]
quicksort (h : t) = quicksort [ x | x <- t; x <= h ] ++ [ h ] ++ quicksort [ x | x <- t; x > h ]
Hier bedeuten:
[]
h:t
[ x | x <- t; x <= h ]
++
leere Liste,
Liste mit Kopf h wie head und Rumpf t wie tail,
Liste aller x aus t, die kleinergleich h sind,
Konkatentation von Listen.
Der kompakten Formulierung der Lösung stehen Effizienzverluste durch das dynamische Erzeugen der vielen Teillisten gegenüber.
Wir ordnen auch die Quicksort-Prozedur der Werkzeugkastenkomponente von 4.1 zu.
Als Testtreiber können wir die Testtreiber zum binären Suchen verwenden, da sie die
Reihungen vor dem Suchen sortieren müssen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.2 Sortieren einer Reihung mit Quicksort
4.2.1
Programm 4.13
Component Pascal:
Quicksort, rekursiv
4 – 23
Component Pascal
MODULE I3ToolsetForArrayOfInteger;
(* Other stuff here. *)
PROCEDURE Quicksort* (VAR array : ARRAY OF Comparable);
(*!
Effect: Sort the whole array. Recursive variant.
!*)
PROCEDURE SortBetween (low, high : Index);
(* Sort the array slice [low .. high].
Choose an arbitrary separation element contained in the slice and repeat
swapping two elements being less and greater than the separation element. The
result are two subslices: The left (right) subslice contains all elements that are less
(greater) than or equal to the separation element. The subslices are sorted
recursively this way.
*)
VAR
left,
right
: Index;
separator : Comparable;
BEGIN
ASSERT ((0 <= low) & (low <= high) & (high < LEN (array)), BEC.precondition);
left
:= low;
right
:= high;
separator := array [MR.UniformI (low, high)];
(* The random choice is inefficient, but it shows that the position of the separator
is meaningless. Accelerate e.g. by using the middle index (low + high) DIV 2.
*)
REPEAT
WHILE array [left] < separator DO
INC (left);
END;
WHILE separator < array [right] DO
DEC (right);
END;
(* ASSERT ((array [left] >= separator) & (separator >= array [right])); *)
IF left <= right THEN
Swap (array, left, right);
INC (left);
DEC (right);
END;
UNTIL right < left;
(* ASSERT (FOR i IN {low..right} : array [i] <= separator); *)
(* ASSERT (FOR i IN {left..high} : separator <= array [i]); *)
IF low < right THEN
SortBetween (low, right);
END;
IF left < high THEN
SortBetween (left, high);
END;
ASSERT (SortedBetween (array, low, high), BEC.postcondition);
END SortBetween;
BEGIN
SortBetween (0, LEN (array) - 1);
ASSERT (Sorted (array), BEC.postcondition);
END Quicksort;
END I3ToolsetForArrayOfInteger.
Da in Component Pascal stets LEN (array) > 0 ist, gilt bei SortBetween die Vorbedingung
low <= high, und als Schleife lässt sich die fußgesteuerte REPEAT-Schleife einsetzen. In
den anderen Sprachen trifft dies nicht zu. Da die beiden rekursiven Aufrufe von SortBet27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 24
4 Reihungen und Abstraktionen
aufeinander folgen, ist SortBetween nicht endrekursiv, sodass das Umformungsschema von S. 3-5 nicht anwendar ist.
ween
4.2.2
Eiffel
Aufgabe 4.6
Eiffel: Quicksort,
rekursiv
Entwickeln Sie eine Eiffel-Variante zu dieser Problemlösung als Teil der generischen
Werkzeugkastenklasse TOOLSET_ARRAY_COMPARABLE von Programm 4.4!
4.2.3
C++
Ein generisches rekursives Quicksort zum Sortieren einer Reihung sieht in C++ so aus:
Programm 4.14
C++: Quicksort,
rekursiv, Schnittstelle
mit generischen
Funktionen
// Header file: ToolsetArrayComparableM.hpp
#ifndef TOOLSET_ARRAY_COMPARABLE_M_HPP_
#define TOOLSET_ARRAY_COMPARABLE_M_HPP_
#include <cassert>
#include <vector>
namespace ToolsetArrayComparable {
// Other stuff here.
template <typename T>
void sort_between (std::vector<T> & array, int low, int high)
// Instantiation condition: T is a comparable type.
{
assert(0 <= low && low < high && high < array.size()); // precondition
int left
= low,
right
= high;
T
separator = array[(low + high) / 2];
do {
while (array[left] < separator) ++left;
while (separator < array[right]) --right;
if (left <= right) {
T store
= array[left];
array[left] = array[right];
array[right] = store;
++left;
--right;
}
} while (left <= right);
if (low < right) sort_between(array, low, right);
if (left < high) sort_between(array, left, high);
assert(sorted_between(array, low, high)); // postcondition
}s
template <typename T>
void quicksort (std::vector<T> & array) {
if (array.size() > 1) sort_between(array, 0, array.size() - 1);
assert(sorted(array)); // postcondition
}
} // ToolsetArrayComparable
#endif // TOOLSET_ARRAY_COMPARABLE_M_HPP_
Aufgabe 4.7
C++: Quicksort,
rekursiv mit
generischer Klasse
Schreiben Sie die generischen Funktionen von Programm 4.14 um zu Klassenfunktionen der generischen Klasse ToolsetArrayComparable in der Schnittstellendatei ToolsetArrayComparableC.hpp von Programm 4.7!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.3 Indexbereich mit maximaler Summe
4 – 25
4.2.4
Java
Aufgabe 4.8
Java: Quicksort,
rekursiv
Entwickeln Sie eine Java-Variante zu dieser Problemlösung als Teil der generischen
Werkzeugkastenklasse ToolsetArrayComparable von Programm 4.10!
4.2.5
C#
Aufgabe 4.9
C#: Quicksort, rekursiv
Entwickeln Sie eine C#-Variante zu dieser Problemlösung als Teil der generischen
Werkzeugkastenklasse ToolsetArrayComparable!
4.2.6
Fazit
Zu ergänzen.
4.3
Indexbereich mit maximaler Summe
Die Lösung des folgenden Problems ist beispielsweise für Urlauber und Börsenspekulanten interessant. Der Urlauber will wissen, wann das Wetter am schönsten ist. Der
Spekulant will wissen, an welchen Tagen er bestimmte Aktien kaufen und verkaufen
soll, um maximalen Gewinn zu erzielen. Die Lösungen setzen allerdings voraus, dass
die Wetterdaten bzw. täglichen Aktienkursschwankungen schon bekannt sind.
Problem
Maximalsumme
Gegeben:
Eine Reihung array ganzer oder reeller Zahlen.
Gesucht:
Ein zusammenhängender Indexbereich low,.., high, sodass die Summe der
Werte array[low],.., array[high] maximal wird.
Es gibt stets eine Lösung, manchmal mehrere Lösungen. Verlangt man z.B. zusätzlich
den Indexbereich mit größtem unterem Index low und dann dem kleinstem oberen
Index high mit low ≤ high, so ist die Lösung eindeutig und es handelt sich mathematisch wie bei den Problemen 3.1 S. 3-1 und 4.1 um eine Abbildung, etwa
range_with_max_sum : n → 2 × .
Als Zahlentyp wählen wir im Folgenden exemplarisch Ganzzahlen. Eine passende
Lösung ist eine Prozedur mit der Reihung als Eingabeparameter und drei Ausgabeparametern für die beiden Indizes und die maximale Summe. Da nicht alle Auswahlsprachen Ausgabeparameter kennen, sind alternative Lösungen zu suchen.
4.3.1
Ersetzen von Ausgabeparametern
Als Alternative zur prozeduralen Lösung mit Ausgabeparametern stellen wir eine
objektorientierte Lösung nach dem Aktion-Abfrage-Schema vor, bei dem Ausgabeparameter durch Attribute oder Datenelemente der Klasse ersetzt sind. Es handelt sich um
die objektorientierte Variante des konventionellen Ersetzens von Referenzparametern
durch globale Variablen. Frühe Programmiersprachen kannten nur parameterlose Prozeduren; diese mussten über globale Variablen kommunizieren. Tabelle 4.1 zeigt exemplarisch Umformungen von Codefragmenten in Eiffel-ähnlichem Pseudocode mit Eingabe- (in) und Ausgabe- (out) Parametern in äquivalente Fragmente, die ohne
Parameter auskommen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 26
Tabelle 4.1
Ersetzen von
Parametern durch
globale Variablen
4 Reihungen und Abstraktionen
Parameterübergabe
Äquivalentes Programm ohne Parameterübergabe
x : INT
p (in x : INT) : INT is
do
result := x * x
end
p : INT is
do
result := x * x
end
q is
local
a : INT
do
a := p (2)
end
q is
local
a : INT
do
x := 2
a := p
end
x : INT
4.3.2
p (out x : INT) is
do
x := 1
end
p is
do
q is
local
a : INT
do
p (a)
end
q is
local
a : INT
do
p
a := x
end
x := 1
end
Component Pascal
Die Component-Pascal-Variante realisieren wir als Prozedur mit Ausgabeparametern.
Der effiziente Algorithmus stammt aus D. Gries: A Note on the Standard Strategy for
Developing Loopinvariants and Loops, Science of Computer Programming 2, S. 207214. Gibt es mehrere Lösungen, so liefert er irgendeine. Der Algorithmus wird auch als
Abtastalgorithmus (scan-line-algorithm) bezeichnet.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.3 Indexbereich mit maximaler Summe
Programm 4.15
Component Pascal:
Indexbereich mit
maximaler Summe
4 – 27
MODULE I3ToolsetForArrayOfInteger;
(* Other stuff here. *)
PROCEDURE DetermineRangeWithMaxSum*
(IN array : ARRAY OF Numeric;
OUT maxLow, maxHigh : Index; OUT maxSum : Numeric);
(*!
Postcondition:
0 <= maxLow <= maxHigh < LEN (array).
maxLow, maxHigh such that array [maxLow] + ... + array [maxHigh] = maximal.
maxSum = array [maxLow] + ... + array [maxHigh].
!*)
VAR
low,
high : Index;
sum : Numeric;
BEGIN
low
:= 0;
sum
:= array [low];
maxLow := low;
maxHigh := low;
maxSum := sum;
FOR high := low + 1 TO LEN (array) - 1 DO
IF sum <= 0 THEN
low := high;
sum := 0;
END;
INC (sum, array [high]);
IF sum > maxSum THEN
maxLow := low;
maxHigh := high;
maxSum := sum;
END;
END;
END DetermineRangeWithMaxSum;
END I3ToolsetForArrayOfInteger.
4.3.3
Eiffel
Verallgemeinern
In
Eiffel
sehen
wir
eine
neue
generische
Werkzeugkastenklasse
TOOLSET_ARRAY_COMPARABLE_NUMERIC vor. Das Problem ist mit einem generischen Elementtyp E lösbar, für den eine Vollordnung und eine Addition definiert sind.
Daher genügt es zu fordern, dass E von den Standardbibliotheksklassen COMPARABLE
(für die Ordnungsrelationen) und NUMERIC (für die arithmetischen Operationen) erbt,
d.h. der formale generische Parameter wird durch eine mehrfache Vererbungsbeziehung eingeschränkt:1
E -> {COMPARABLE, NUMERIC}
Codevererbung
Da mit Reihungen vergleichbarer numerischer Elemente alles machbar ist, was mit
Reihungen
vergleichbarer
Elemente
machbar
ist,
lassen
wir
TOOLSET_ARRAY_COMPARABLE_NUMERIC von TOOLSET_ARRAY_COMPARABLE
erben – ein Beispiel für Codevererbung: Beide Klassen sind konkret, d.h. vollständig
implementiert; TOOLSET_ARRAY_COMPARABLE_NUMERIC übernimmt alle geerbten
Merkmale ohne Redefinitionen.
Ausgabeparameter
ersetzen
Die Kommunikation modellieren wir nach dem Kommando-Abfrage-Schema: Die
Ausgabeparameter der Prozedur der Component-Pascal-Variante ersetzen wir durch
1
Dieses Sprachmerkmal ist im ECMA-Standard [ECMA367] in 8.12.8 enthalten, aber vielleicht nicht in jeder Sprachimplementation (Zugriff 2006-04-07).
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 28
4 Reihungen und Abstraktionen
entsprechende Attribute. Die Prozedur reduziert sich auf ein parameterloses Kommando. Der Effekt eines Kommandoaufrufs lässt sich nach dem Aufruf durch Abfragen
der Attribute ermitteln.
Bild 4.2
Klassendiagramm zu
Werkzeugkastenklasse
n
TOOLSET_ARRAY_COMPARABLE [E -> COMPARABLE]
query
sorted
has
command
gapsort
quicksort
TOOLSET_ARRAY_COMPARABLE_NUMERIC [E -> {COMPARABLE, NUMERIC}]
Trennen von Abfragen
und Kommandos
Programm 4.16
Eiffel: Indexbereich mit
maximaler Summe
query
max_low
max_high
max_sum
command
determine_range_with_max_sum
Unsere Klassendiagramme sind nicht konform zur UML. Indem wir zwischen Abfragen und Kommandos unterscheiden, betonen wir einen an Klassenschnittstellen wichtigen semantischen Aspekt. Demgegenüber ist die Unterscheidung zwischen Attributen
und Methoden ein Implementationsaspekt, der beim Entwurf von Klassenschnittstellen
keine Rolle spielen sollte. Attribute, die nicht schreibgeschützt exportiert werden können, sollten nicht zu Schnittstellen gehören. Einer parameterlosen Abfrage ist dagegen
nicht anzusehen, ob sie als Attribut oder Funktion implementiert wird. Dies ist ein
Aspekt der Bezugstranparenz (siehe S. 1-10).
note
description: "Tools operating on arrays of comparable numeric elements"
class TOOLSET_ARRAY_COMPARABLE_NUMERIC [E -> {COMPARABLE, NUMERIC}]
inherit
TOOLSET_ARRAY_COMPARABLE [E]
feature
max_low,
max_high : INTEGER
max_sum : E
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.3 Indexbereich mit maximaler Summe
4 – 29
determine_range_with_max_sum (array : ARRAY [E])
require
array_exists: array /= Void
local
low,
high : INTEGER
sum : E
do
from
low
:= array.lower
high
:= low + 1
sum
:= array.item (low)
max_low := low
max_high := low
max_sum := sum
until high > array.upper loop
if sum <= zero then
low := high
sum := zero
end
sum := sum + array.item (high)
if sum > max_sum then
max_low := low
max_high := high
max_sum := sum
end
high := high + 1
end
ensure
low_large_enough: array.lower <= max_low
range_valid:
max_low <= max_high
high_small_enough: max_high <= array.upper
-- range_has_maximal_sum:
max_low, max_high such that
-array.item (max_low) + ... + array.item (max_high) = maximal
-- maximal_sum: max_sum = array.item (max_low) + ... + array.item (max_high)
end -- determine_range_with_max_sum
end -- class TOOLSET_ARRAY_COMPARABLE_NUMERIC
Implizite Initialisierung
TOOLSET_ARRAY_COMPARABLE_NUMERIC braucht keine Erzeugungsprozedur, weil
Attribute max_low, max_high, max_sum erst nach einem Aufruf von
determine_range_with_max_sum definierte Werte haben müssen. Bei der Objekterzeu-
die
gung werden die Attribute implizit mit Standardwerten – konkret mit Nullen – initialisiert.
Aufgabe 4.10
Eiffel: Testtreiber zu
Indexbereich mit
maximaler Summe
Entwickeln Sie einen Testtreiber zur Eiffel-Variante dieses Problems!
4.3.4
C++
Erben oder
Inkludieren?
Da wir in C++ für Ausgabeparameter Referenzen übergeben können, verzichten wir
auf das Aktion-Abfrage-Schema. Für die erste C++-Variante sehen wir eine neue
Schnittstellendatei ToolsetArrayComparableNumericM.hpp vor. Da mit Reihungen vergleichbarer numerischer Elemente alles machbar ist, was mit Reihungen vergleichbarer
Elemente machbar ist, inkludieren wir ToolsetArrayComparableM.hpp in ToolsetArray-
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 30
4 Reihungen und Abstraktionen
ComparableNumericM.hpp. Somit benutzen wir Inkludierung als primitive Form der
Codevererbung.
Programm 4.17
C++: Indexbereich mit
maximaler Summe,
Schnittstelle
// Header file: ToolsetArrayComparableNumericM.hpp
#ifndef TOOLSET_ARRAY_COMPARABLE_NUMERIC_M_HPP_
#define TOOLSET_ARRAY_COMPARABLE_NUMERIC_M_HPP_
#include <cassert>
#include <vector>
#include "ToolsetArrayComparableM.hpp"
namespace ToolsetArrayComparable {
template <typename T>
void determine_range_with_max_sum
(const std::vector<T> & array, int & max_low, int & max_high, T & max_sum)
// Instantiation condition: T is a comparable numeric type.
{
int low = 0;
T sum = array[low];
max_low = low;
max_high = low;
max_sum = sum;
for (int high = low + 1; high < array.size(); ++high) {
if (sum <= 0) {
low = high;
sum = 0;
}
sum += array[high];
if (sum > max_sum) {
max_low = low;
max_high = high;
max_sum = sum;
}
}
assert(0 <= max_low);
// postcondition
assert(max_low <= max_high); // postcondition
assert(max_high < array.size()); // postcondition
}
} // ToolsetArrayComparable
#endif // TOOLSET_ARRAY_COMPARABLE_NUMERIC_M_HPP_
Aufgabe 4.11
C++: Indexbereich mit
maximaler Summe als
Klassenfunktion
Entwickeln Sie eine C++-Variante dieses Problems mit determine_range_with_max_sum
als Klassenfunktion einer generischen Klasse ToolsetArrayComparableNumeric, die von
ToolsetArrayComparable von Programm 4.7 erbt!
Aufgabe 4.12
C++: Testtreiber zu
Indexbereich mit
maximaler Summe
Entwickeln Sie Testtreiber zu den C++-Varianten dieses Problems!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.3 Indexbereich mit maximaler Summe
4.3.5
4 – 31
Java
Da Java keine Ausgabeparameter kennt, wenden wir das in der Eiffel-Variante demonstrierte Aktion-Abfrage-Schema an.
Programm 4.18
Java: Indexbereich mit
maximaler Summe
public class ToolsetArrayInteger {
/* Other stuff here. */
private int
maxLow,
maxHigh;
private int
maxSum;
public int maxLow () {
return maxLow;
}
public int maxHigh () {
return maxHigh;
}
public int maxSum () {
return maxSum;
}
public void determineRangeWithMaxSum (int[] array) {
assert array != null : "precondition";
int low = 0;
int sum = array[low];
maxLow = low;
maxHigh = low;
maxSum = sum;
for (int high = low + 1; high < array.length; ++high) {
if (sum <= 0) {
low = high;
sum = 0;
}
sum += array[high];
if (sum > maxSum) {
maxLow = low;
maxHigh = high;
maxSum = sum;
}
}
assert 0 <= maxLow()
: "postcondition";
assert maxLow() <= maxHigh()
: "postcondition";
assert maxHigh() < array.length : "postcondition";
}
}
Datenfelder und Funktionen sind nicht static vereinbart, damit das Aktion-AbfrageMuster flexibel nutzbar ist.
Aufgabe 4.13
Java: Indexbereich mit
maximaler Summe
Entwickeln Sie eine Java-Variante dieses Problems mit einer generischen Klasse ToolsetArrayComparableNumeric, die von ToolsetArrayComparable von Programm
4.10 erbt! Entwickeln Sie auch einen Testtreiber dazu!
4.3.6
C#
Aufgabe 4.14
C#: Indexbereich mit
maximaler Summe
Entwickeln Sie eine C#-Variante zu dieser Problemlösung! Entwickeln Sie auch einen
Testtreiber dazu!
4.3.7
Fazit
Zu ergänzen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 32
4.4
4 Reihungen und Abstraktionen
Abstrakte, generische und konkrete Testtreiber zu den
Werkzeugkästen
Bisher haben wir zu den Problemen 4.1 bis 4.3 jeweils einzelne Testtreiber geschrieben. Die Copy-&-Paste-Methode führt zwar schnell zu Lösungen, repliziert aber Quellcode, was die Wartbarkeit der Testtreiber reduziert. Ein besserer Entwurf der Testtreiber nutzt statt Codereplikation die Abstraktionsmechanismen Benutzung, Vererbung
und Generizität. Dazu analysieren wir die einfachen Testtreiber, um gemeinsame Teile
zu erkennen und in geeignete abstrakte generische Klassen heraus zu faktorisieren. Die
Aufgaben, die die Testtreiber zu erledigen haben, gliedern wir teilweise horizontal
arbeitsteilig, teilweise vertikal nach allgemeinen und speziellen Teilaufgaben:
Als ausgliederbare Teilaufgabe bietet sich die Initialisierung von Reihungen mit
Zufallswerten an. Das ausgegliederte Teil, der Initialisierer, wird weiter vertikal
mittels Vererbung und Generizität zerlegt.
Die Testtreiber delegieren das Initialisieren von Reihungen an den Initialisierer und
benutzen einen Werkzeugkasten. So bleiben ihnen sonst nur die Aufgaben, Kommandoargumente zu übernehmen und Testergebnisse auszugeben. Auch die Testtreiber gliedern wir vertikal mittels Vererbung und Generizität.
4.4.1
Eiffel
Die folgenden Klassendiagramme stellen den Entwurf der Eiffel-Variante dar. Dieser
lässt sich auf Entwurfsvarianten anderer Sprachen mit Generizität übertragen. Ohne
Generizität muss man freilich auf gewisse Abstraktionen verzichten. Merkmale in der
linken Spalte sind öffentlich, Merkmale in der rechten Spalte geschützt.
4.4.1.1
Initialisierer
Bild 4.3
Klassendiagramm zur
Initialisierung von
Reihungen
INITIALIZER_OF_ARRAY [E -> COMPARABLE] abstract
query
min
max
command
init_random
make
random
random_item abstract
E = STRING
INITIALIZER_OF_ARRAY_STRING
random_item
query
E = REAL
INITIALIZER_OF_ARRAY_REAL
query
random_item
Die Initialisiererklasse INITIALIZER_OF_ARRAY soll einerseits flexibel Reihungen mit
beliebigen Elementtypen initialisieren können, andererseits weit gehend fertig implementiert sein, d.h. es sollen möglichst wenig Implementationen fehlen. Zwecks Variation des Elementtyps ist die Klasse generisch, zwecks Offenlassen von Implementationen abstrakt.
Schnittstellen- und
Implementationsklasse
INITIALIZER_OF_ARRAY ist eine Schnittstellenklasse, d.h. sie bietet eine vollständige
Schnittstelle, die unvollständig implementiert ist. Hier ist nur die Abfrage random_item
abstrakt,
also
noch
nicht
implementiert.
Nachfolgerklassen
von
INITIALIZER_OF_ARRAY konkretisieren den generischen Typ, können dadurch
random_item implementieren und werden so zu Implementationsklassen, die nur die
geerbte Schnittstelle implementieren, ohne sie durch weitere Merkmale zu erweitern.
Solche Implementationsklassen eignen sich zur polymorphen Benutzung.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen
Programm 4.19
Eiffel: Abstrakte
generische
Initialisierung von
Reihungen
vergleichbarer
Elemente
4 – 33
note
description: "Abstract array initialization using the template method design pattern"
deferred class INITIALIZER_OF_ARRAY [E -> COMPARABLE]
feature {NONE}
random : RANDOM
once
create Result.make
ensure
random_exists: Result /= Void
end -- random
item : E
forth
-- Give item another random value.
deferred
end -- forth
feature
min : E
max : E
init_random (array : ARRAY [E])
-- Initialize array with random values between min and max.
require
array_exists: array /= Void
local
index : INTEGER
do
from
index := array.lower
until index > array.upper loop
array.put (item, index)
forth
index := index + 1
end
end -- init_random
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 34
4 Reihungen und Abstraktionen
make (new_min, new_max : E)
require
new_min_exists:
new_min /= Void
new_max_exists:
new_max /= Void
new_bounds_consistent: new_min <= new_max
do
min := new_min
max := new_max
forth
ensure
min_defined:
min = new_min
max_defined:
max = new_max
end -- make
invariant
random_exists:
min_exists:
max_exists:
item_exists:
item_large_enough:
item_small_enough:
random /= Void
min /= Void
max /= Void
item /= Void
min <= item
item <= max
end -- class INITIALIZER_OF_ARRAY
Abstrakte Klassen heißen in Eiffel aufgeschoben (deferred) und werden mit deferred
markiert. Entsprechend tritt bei abstrakten Routinen an die Stelle des mit do eingeleiteten Anweisungsteils das deferred.
Entwurfsmuster
Schablonenmethode
Die abstrakte generische Klasse INITIALIZER_OF_ARRAY erfüllt ihre Aufgabe, Reihungen mit Zufallswerten zu initialisieren, mit der Routine init_random, die komplett implementiert ist. init_random benutzt allerdings die abstrakte Prozedur forth. Im Katalog
der objektorientierten Entwurfsmuster heißt eine vollständig implementierte Methode,
die abstrakte Methoden aufruft, Schablonenmethode (template method) [GHJV04].
forth gibt item einen Zufallswert des generischen Typs E zwischen den Grenzen min und
max. Zwecks flexibler Benutzbarkeit übernimmt die Erzeugungsprozedur make die
Bereichsgrenzen als Parameter und speichert sie. forth kann ein durch die Einmalfunktion random erzeugtes RANDOM-Objekt benutzen. Nachfolgerklassen von
INITIALIZER_OF_ARRAY müssen nur noch forth implementieren.
Programm 4.20
Eiffel: Initialisierung
gleitpunktzahliger
Reihungen
note
description: "Real array initialization"
class INITIALIZER_OF_ARRAY_REAL
inherit
INITIALIZER_OF_ARRAY [REAL]
create
make
feature {NONE}
forth
do
item := ((random.item - 1) / (random.modulus - 2)) * (max - min) + min
end -- forth
end -- class INITIALIZER_OF_ARRAY_REAL
Die
Implementationsklasse
INITIALIZER_OF_ARRAY_REAL
erbt
von
INITIALIZER_OF_ARRAY und konkretisiert den generischen Parameter E zu REAL. Im
Vererbungsabschnitt ist anzugeben, dass forth redefiniert wird. Die Implementation von
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen
4 – 35
forth ähnelt der von random_item in Programm 4.5. Mehr Aufwand fordert die Implementationsklasse INITIALIZER_OF_ARRAY_STRING, die Reihungen mit zufällig langen
zufälligen Zeichenketten initialisieren soll.
Programm 4.21
Eiffel: Initialisierung
von Reihungen von
Zeichenketten
note
description: "String array initialization"
class INITIALIZER_OF_ARRAY_STRING
inherit
INITIALIZER_OF_ARRAY [STRING]
create
make
feature {NONE}
max_string_capacity : INTEGER
once
Result := min.capacity.max (max.capacity)
end -- Max_string_capacity
max_value : INTEGER = 61
-- = 2 * 26 + 10 - 1, but constant expression not allowed here!
random_alpha_numeric : CHARACTER
local
value : INTEGER
do
value := random.item \\ (max_value + 1)
inspect
value
when 0 .. 9 then
Result := (value + ('0').code).to_character
when 10 .. 35 then
Result := (value + ('A').code - 10).to_character
when 36 .. max_value then
Result := (value + ('a').code - 36).to_character
else
check unreachable_branch: False end
end
ensure
ok: Result.is_alpha or Result.is_digit
end -- random_alpha_numeric
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 36
4 Reihungen und Abstraktionen
forth
local
i : INTEGER
do
from
random.forth
create item.make_filled ('?', random.item \\ max_string_capacity + 1)
until min <= item and item <= max loop
-- Danger of infinite loop!
from
i := item.index_set.lower
until i > item.index_set.upper loop
random.forth
item.put (random_alpha_numeric, i)
i := i + 1
end
end
end -- forth
end -- class INITIALIZER_OF_ARRAY_STRING
forth soll für item eine zufällig lange zufällige Zeichenkette produzieren. Zunächst
erzeugt forth eine mit '?' gefüllte Zeichenkette, die höchstens so lang wie die beschränkenden Zeichenketten min und max ist. Dann ersetzt es die '?' mit random_alpha_numeric
durch zufällige alphanumerische Zeichen. Da die produzierte Zeichenkette nicht sicher
durch min und max beschränkt ist, wiederholt forth das Ersetzen. Je näher die Schranken
beieinander liegen, desto länger kann es dauern, bis eine korrekte Zeichenkette produziert ist – vielleicht ewig. Bessere Algorithmen sind möglich.
Mehrseitige Auswahl
random_alpha_numeric wandelt die aktuelle Zufallszahl in eine Ziffer, einen Klein- oder
Großbuchstaben. Dazu benutzt es die mehrseitige Auswahlanweisung, die in Eiffel mit
dem Schlüsselwort inspect beginnt. Wie die CASE-Anweisung von Component Pascal
entspricht die inspect-Anweisung der 1-Eingang-/1-Ausgang-Regel strukturierten Programmierens und erlaubt wie jene, mehrere Fälle durch Aufzählungen und Intervalle
zusammenzufassen. Doch während die CASE-Anweisung bei Fällen konstante Ausdrücke zulässt, dürfen in der inspect-Anweisung dort nur Konstanten vorkommen, z.B. ist
das Fallintervall
when 36 .. number_of_alpha_numeric_chars - 1 then
illegal. Dieser Nachteil folgt aus der allgemeinen Schwäche von Eiffel, Konstanten
nicht gut zu unterstützen. Beispielsweise ist die Konstantenvereinbarung
number_of_alpha_numeric_chars : INTEGER = 2 * 26 + 10
nicht möglich, da rechts nur Konstanten, keine konstanten Ausdrücke erlaubt sind. Der
Programmierer kann entweder gegen die Programmierleitlinie, Abhängigkeiten explizit
auszudrücken, verstoßen und selbst ausrechnen, was sie dem Kompilierer überlassen
will, oder auf die Konstante verzichten und ihren Wert mit einer Einmalfunktion implementieren:
number_of_alpha_numeric_chars : INTEGER
once
Result := 2 * 26 + 10
end
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen
4.4.1.2
4 – 37
Testtreiber
Bild 4.4 Klassendiagramm zur Testtreibern
TESTER_OF_TSAC [E -> COMPARABLE] abstract
query
tsac
array
ia
command
make abstract
create_ia abstract
init
E = STRING
TESTER_OF_TSAS abstract
TESTER_OF_TSAR abstract
tsac
query
tsac
command
create_ia
command
create_ia
command
make
INITIALIZER_OF_ARRAY
[E -> COMPARABLE]
E = REAL
query
TESTER_OF_TSAS_HAS
TOOLSET_ARRAY_COMPARABLE
[E -> COMPARABLE]
TESTER_OF_TSAR_DETERMINE
command
make
Die Testerklasse TESTER_OF_TSAC soll einerseits flexibel verschiedene Werkzeuge für
Reihungen mit beliebigen Elementtypen testen können, andererseits fast fertig implementiert sein. Wie die Initialisiererklasse ist sie zur Variation des Elementtyps generisch, zum Offenlassen von Implementationen abstrakt. Sie wird aber über zwei Stufen
konkretisiert:
Der abstrakte generische Testtreiber TESTER_OF_TSAC ist eine Schnittstellenklasse.
Sie benutzt die allgemeinere generische Werkzeugkastenklasse und die abstrakte
generische Initialisiererklasse.
Da der Elementtyp E generisch ist, kann hier kein Initialisiererobjekt erzeugt
werden. Deshalb ist dafür die abstrakte Prozedur create_ia vorgesehen.
Offen bleibt, welches Werkzeug zu testen ist. Dafür ist die abstrakte Prozedur
make vorgesehen.
Der erste Konkretisierungsschritt liefert eine Schnittstellenklasse mit konkretisiertem Elementtyp E, z.B. TESTER_OF_TSAS für STRING. Damit ist create_ia implementierbar. Außerdem lässt sich der Typ des benutzten Werkzeugkastenobjekts tsac
genau festlegen.
Der zweite Konkretisierungsschritt liefert eine Implementationsklasse, die das
getestete Werkzeug festlegt, z.B. TESTER_OF_TSAS_HAS die binäre Suchfunktion
has. Sie implementiert make mit einem geeigneten Algorithmus.
Programm 4.22
Eiffel: Abstrakter
generischer Testtreiber
zu Reihungen
vergleichbarer
Elemente
27.9.12
note
description: "Abstract generic test driver for class TOOLSET_ARRAY_COMPARABLE"
deferred class TESTER_OF_TSAC [E -> COMPARABLE]
feature {NONE}
tsac
: TOOLSET_ARRAY_COMPARABLE [E]
array : ARRAY [E]
ia
: INITIALIZER_OF_ARRAY [E]
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 38
4 Reihungen und Abstraktionen
create_ia (min, max : E)
require
min_exists:
min /= Void
max_exists:
max /= Void
bounds_consistent: min <= max
deferred
ensure
ia_exists: ia /= Void
end -- create_ia
init (size : INTEGER; min, max : E)
require
size_positive:
size > 0
min_exists:
min /= Void
max_exists:
max /= Void
bounds_consistent: min <= max
do
create tsac
create array.make (1, size)
create_ia (min, max)
ia.init_random (array)
io.put_string ("array: "); print (array)
ensure
tsac_exists: tsac /= Void
array_exists: array /= Void
ia_exists:
ia /= Void
end -- init
feature
make
deferred
end -- make
end -- class TESTER_OF_TSAC
Erläuterungen zu ergänzen.
Programm 4.23
Eiffel: Abstrakter
Testtreiber zu
Reihungen von
Zeichenketten
note
description: "Abstract test driver for type TOOLSET_ARRAY_COMPARABLE [STRING]"
deferred class TESTER_OF_TSAS
inherit
TESTER_OF_TSAC [STRING]
feature {NONE}
create_ia (min, max : STRING)
do
create {INITIALIZER_OF_ARRAY_STRING} ia.make (min, max)
end -- create_ia
end -- class TESTER_OF_TSAS
Die Initialisierergröße ia soll polymorph benutzt werden und behält daher ihren abstrakten Typ INITIALIZER_OF_ARRAY [E], der zu INITIALIZER_OF_ARRAY [STRING]
konkretisiert ist. Das redefinierte Kommando create_ia erzeugt mit der modifizierten
create-Anweisung
ein
Objekt
des
passenden
konkreten
Typs
INITIALIZER_OF_ARRAY_STRING. Analog wird create_ia in der REAL-Variante redefiniert.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen
Programm 4.24
Eiffel: Abstrakter
Testtreiber zu
gleitpunktzahligen
Reihungen
4 – 39
note
description: "Abstract test driver for type
TOOLSET_ARRAY_COMPARABLE_NUMERIC [REAL]"
deferred class TESTER_OF_TSAR
inherit
TESTER_OF_TSAC [REAL]
redefine
tsac
end
feature {NONE}
tsac : TOOLSET_ARRAY_COMPARABLE_NUMERIC [REAL]
create_ia (min, max : REAL)
do
create {INITIALIZER_OF_ARRAY_REAL} ia.make (min, max)
end -- create_ia
end -- class TESTER_OF_TSAR
Die REAL-Variante redefiniert zusätzlich das mit einem Werkzeugkastenobjekt zu verbindende tsac. Diese Redefinition ist nur eine konforme Typänderung. Da der aktuelle
generische Typ REAL vergleichbar und numerisch ist, kann das Werkzeugkastenobjekt
anstelle des ursprünglich vorgesehenen Typs TOOLSET_ARRAY_COMPARABLE [E] den
erweiterten Typ TOOLSET_ARRAY_COMPARABLE_NUMERIC [REAL] annehmen. Damit
bietet tsac die entsprechend erweiterte Schnittstelle.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 40
Programm 4.25
Eiffel: Testtreiber zu
binärem Suchen in
Reihungen von
Zeichenketten
4 Reihungen und Abstraktionen
note
description: "Test driver for feature TOOLSET_ARRAY_COMPARABLE [STRING].has"
class TESTER_OF_TSAS_HAS
inherit
TESTER_OF_TSAS
create
make
feature
make
-- Given one integer and three string command arguments with the meaning
-- 1. size > 0,
-- 2. min,
-- 3. max with min <= max,
-- 4. item,
-- fill a string array of size size with random values between min and max and
-- check if it contains item.
local
command_line : ARGUMENTS
size
: INTEGER
min,
max,
item
: STRING
do
create command_line
if command_line.argument_count < 4 or else
not command_line.argument (1).is_integer or else
command_line.argument (1).to_integer <= 0 or else
command_line.argument (2) > command_line.argument (3)
then
io.put_string ("Please call this command with 1 integer, 3 string arguments")
io.put_string (" size (>0), min, max (>= min), item!")
else
size := command_line.argument (1).to_integer
min := command_line.argument (2)
max := command_line.argument (3)
item := command_line.argument (4)
init (size, min, max)
tsac.sort (array)
io.put_string (" item " + item)
if not tsac.has (array, item) then
io.put_string (" not")
end
io.put_string (" found in array.")
end
io.put_new_line
end -- make
end -- class TESTER_OF_TSAS_HAS
Im Unterschied zum ursprünglichen einfachen Testtreiber TESTER_OF_TSACI_HAS
Programm 4.5 befasst sich TESTER_OF_TSAS_HAS fast nur noch mit Ein-/Ausgabe;
sein funktionaler Teil beschränkt sich auf die Aufrufe init (size, min, max),
tsac.sort (array) und tsac.has (array, item).
Um andere Testtreiber zu erhalten, können wir auf die Copy-&-Paste-Methode nicht
ganz verzichten. Für Testtreiber für Reihungen mit anderen Elementtypen sind an Programm 4.25
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen
4 – 41
die Vererbungsbeziehung,
das Prüfen und Konvertieren der Kommandoargumente und
das Ausgeben der Ergebnisse
anzupassen. Zum Testen anderer Merkmale der Werkzeugkastenklassen sind die Aufrufe dieser Merkmale anzupassen. Exemplarisch zeigen wir einen Testtreiber für den
Indexbereich mit maximaler Summe.
Programm 4.26
Eiffel: Testtreiber zu
Indexbereich mit
maximaler Summe in
gleitpunktzahligen
Reihungen
note
description: "Test driver for feature
TOOLSET_ARRAY_COMPARABLE_NUMERIC
[REAL].determine_range_with_max_sum"
class TESTER_OF_TSAR_DETERMINE
inherit
TESTER_OF_TSAR
create
make
feature
make
-- Given an integer and two real command arguments with the meaning
-- 1. size > 0,
-- 2. min < 0
-- 3. max with min <= max,
-- fill a real array of size size with random values between min and max and
-- determine a range with maximal sum.
local
command_line : ARGUMENTS
size
: INTEGER
min,
max
: REAL
do
create command_line
if command_line.argument_count < 3 or else
not command_line.argument (1).is_integer or else
command_line.argument (1).to_integer <= 0 or else
not command_line.argument (2).is_real or else
not command_line.argument (3).is_real or else
command_line.argument (2).to_real > command_line.argument (3).to_real
then
io.put_string ("Please call this command with an integer and two real")
io.put_string (" arguments size (>0), min, max (>= min)!")
else
size := command_line.argument (1).to_integer
min := command_line.argument (2).to_real
max := command_line.argument (3).to_real
init (size, min, max)
tsac.determine_range_with_max_sum (array)
io.put_string (" max_low: "); io.put_integer (tsac.max_low)
io.put_string (" max_high: "); io.put_integer (tsac.max_high)
io.put_string (" max_sum: "); io.put_real (tsac.max_sum)
end
io.put_new_line
end -- make
end -- class TESTER_OF_TSAR_DETERMINE
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 42
4.4.2
4 Reihungen und Abstraktionen
C++
Prinzipiell kann man für die C++-Variante den in Bild 4.3 dargestellten Entwurf der
Initialisiererklassen und den in Bild 4.4 dargestellten Entwurf der Testtreiber übernehmen – es ist aber ein Stück harter Arbeit im Detail. Deshalb sei diese Aufgabe dem
interessierten Leser zur Übung überlassen. Hier vereinfachen wir den Entwurf in folgenden Aspekten, wobei wir freilich einige Abstraktionen verlieren:
Initialisiererklasse
Testerklasse
tsac
ia, create_ia
make
create_ia, init
Testerhauptprogramm
Eine statt drei Initialisiererklassen: Das bietet sich solange an, wie wir uns bei den
exemplarischen Testtreibern auf die Typen int und float beschränken. Die C++-Initialisiererklasse InitializerOfArray ist dann wie die Eiffel-Initialisiererklasse
INITIALIZER_OF_ARRAY generisch, aber nicht abstrakt, weil wir random_item sofort
implementieren, wobei die Implementation eine explizite Typkonversion in den
generischen Typ verwendet. Typkonversionen sind in Eiffel verpönt, in C/C++
beliebt. Die Implementation von random_item passt zu den Typen int und float, aber
freilich nicht zu string. So sparen wir die Definition von zwei Ableitungsklassen
(auf Kosten der Chance, abstrakte Klassen und Vererbung in C++ kennen zu lernen). Falls die Standardimplementation von random_item zu einem aktuellen generischen Typ nicht passt, so ist random_item in einer Ableitungsklasse passend zu redefinieren oder besser eine Abstraktionsklasse einzuführen. Die Allgemeinheit des
Entwurfs ist also nicht eingeschränkt.
Eine konkrete statt drei abstrakte Testerklassen: Die C++-Testerklasse TesterOfTSAC
ist wie die Eiffel-Testerklasse TESTER_OF_TSAC generisch, aber aus mehreren
Gründen nicht abstrakt:
Bei der modularen Variante des Werkzeugkastens entfällt die Notwendigkeit, ein
passendes Objekt tsac zu erzeugen. tsac wird zu einem Aliasnamen für den
Namensraum des Werkzeugkastens.
Solange es nur eine konkrete Initialisiererklasse gibt, entfällt die Notwendigkeit
der polymorphen Erzeugung eines passenden Initialisiererobjekts ia. Statt des
abstrakten create_ia mit zwei Konkretisierungen genügt ein konkretes create_ia.
Somit können wir auf die abstrakten Testerklassen TESTER_OF_TSAS und
TESTER_OF_TSAR verzichten. Sodann vereinfachen wir weiter:
Da in C++ Konstruktoren nicht abstrakt sein können, ist das abstrakte make sinnlos und entfällt.
Da man in C++ bei Klassen mit Datenfeldern kaum auf Konstruktoren verzichten kann, lassen wir die Aufgaben des nun konkreten create_ia und des schon
konkreten init von einem Konstruktor erledigen.
Die
konkreten
Eiffel-Testerklassen
TESTER_OF_TSAS_HAS
und
TESTER_OF_TSAR_DETERMINE erledigen ihre Aufgaben vollständig in ihren
Erzeugungsprozeduren make. Da C++ nicht ohne main-Funktion auskommt, ist
gemäß diesem Entwurf in einem main ein Objekt einer konkreten Testerklasse zu
erzeugen, dessen Konstruktor die eigentliche Arbeit leistet. Dies ist zwar gängige
Praxis, aber umständlich, weil man für jeden Test eine Klasse mit einem Konstruktor und das unvermeidliche main schreiben muss. Deshalb verzichten wir auf konkrete Testerklassen und lassen ihre Aufgaben direkt von main-Funktionen erledigen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen
4.4.2.1
Programm 4.27
C++: Generische
Initialisierung von
Reihungen
vergleichbarer
Elemente
4 – 43
Initialisierer
// Header file: InitializerOfArray.hpp
#ifndef INITIALIZER_OF_ARRAY_HPP_
#define INITIALIZER_OF_ARRAY_HPP_
#include <cassert>
#include <cstdlib>
#include <iostream>
#include <vector>
template <typename T>
class InitializerOfArray
// Instantiation condition: T is a comparable type.
{
protected:
virtual T random_item ();
T min,
max;
public:
T get_min () {return min;}
T get_max () {return max;}
virtual void init_random (std::vector<T> & array);
virtual void print (const std::vector<T> & array);
InitializerOfArray (T new_min, T new_max);
}; // InitializerOfArray
template <typename T>
T InitializerOfArray<T>::random_item () {
T result = T((double(std::rand() - 1) / (RAND_MAX - 1)) * (max - min + 1) + min);
assert(get_min() <= result); // postcondition
assert(result <= get_max()); // postcondition
return result;
}
template <typename T>
void InitializerOfArray<T>::init_random (std::vector<T> & array) {
for (int index = 0; index < array.size(); ++index) {
array[index] = random_item();
}
}
template <typename T>
void InitializerOfArray<T>::print (const std::vector<T> & array) {
std::cout << "[";
for (int index = 0; index < array.size(); ++index) {
std::cout << array[index] << " ";
}
std::cout << "]" << std::endl;
}
template <typename T>
InitializerOfArray<T>::InitializerOfArray (T new_min, T new_max) {
assert(new_min <= new_max); // precondition
min = new_min;
max = new_max;
}
#endif // INITIALIZER_OF_ARRAY_HPP_
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 44
4 Reihungen und Abstraktionen
Die C++-Initialisiererklasse unterscheidet sich in folgenden Implementationsaspekten
von der Eiffel-Initialisiererklasse:
Lesezugriff
Inlining
Redefinierbarkeit
Konstruktor
Aus den Abfragen min und max werden analog zum Java-Programm 4.18 geschützte
Datenfelder min und max und öffentliche Funktionen get_min und get_max. Im Unterschied zu Java dürfen Felder und parameterlose Funktionen nicht gleich benannt
sein.
Die get-Funktionen sind innerhalb der Klassendefinition als Inline-Funktionen implementiert. Inline-Code wird an Aufrufstellen substituiert.
Funktionen sollten normalerweise als virtual deklariert sein, denn nur so können sie
polymorph verwendet, also redefiniert und dynamisch gebunden werden. In fast
allen objektorientierten Sprachen seit Smalltalk ist polymorphe Benutzung der Standard (ausgenommen objektorientierte Erweiterungen älterer Sprachen). Die spezielle Markierung des Normalfalls mit virtual stammt aus Simula 67 und wurde so in
C++ und leider auch in C# übernommen, aber zum Glück nicht in Java.
Die print-Prozedur war in der Eiffel-Variante nicht nötig. Eine bessere C++-Variante davon würde wie in Programm 4.8 den <<-Operator überladen.
Aus der make-Prozedur wird ein Konstruktor. Konstruktoren dienen der Initialisierung von Objekten, heißen immer wie ihre Klasse, haben keinen Typ und können
überladen, aber nicht abstrakt und nicht virtuell sein. In C++ müssen Ableitungsklassen eigene Konstruktoren haben, während man in Eiffel geerbte Prozeduren zu
Erzeugungsprozeduren erklären kann.
Nun zu den Testerklassen.
4.4.2.2
Programm 4.28
C++: Generischer
Testtreiber zu
Reihungen
vergleichbarer
Elemente
Testtreiber
// Header file: TesterOfTSAC.hpp
#ifndef TESTER_OF_TSAC_HPP_
#define TESTER_OF_TSAC_HPP_
#include <cassert>
#include <iostream>
#include <vector>
#include "InitializerOfArray.hpp"
template <typename T>
class TesterOfTSAC
// Instantiation condition: T is a comparable type.
{
protected:
std::vector<T> * array;
InitializerOfArray * ia;
public:
std::vector<T> & get_array () {return *array;}
InitializerOfArray<T> & get_ia () {return *ia;}
TesterOfTSAC (int size, T min, T max);
}; // TesterOfTSAC
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen
4 – 45
template <typename T>
TesterOfTSAC<T>::TesterOfTSAC (int size, T min, T max) {
assert(size > 0);
// precondition
assert(min <= max); // precondition
array = new std::vector<T>(size);
ia = new InitializerOfArray<T>(min, max);
ia->init_random(*array);
std::cout << "array: "; ia->print(*array);
}
#endif // TESTER_OF_TSAC_HPP_
Erläuterungen zu ergänzen.
Programm 4.29
C++: Testtreiber zu
binärem Suchen in
ganzzahligen
Reihungen
// Implementation file: TesterOfTSAIHas.cpp
#include <cstdlib>
#include <iostream>
#include "ToolsetArrayComparableM.hpp"
#include "TesterOfTSAC.hpp"
namespace tsac = ToolsetArrayComparable;
int main (int argc, char * argv[]) {
if (argc < 4 || std::atoi(argv[1]) <= 0 || std::atoi(argv[2]) > std::atoi(argv[3])) {
std::cout << "Please call this command with four integer arguments"
<< " size (> 0), min, max (>= min), item!";
} else {
int size = std::atoi(argv[1]);
int min = std::atoi(argv[2]);
int max = std::atoi(argv[3]);
int item = std::atoi(argv[4]);
TesterOfTSAC<int> tester = TesterOfTSAC<int>(size, min, max);
tsac::sort(tester.get_array());
std::cout << "item " << item;
if (!tsac::has(tester.get_array(), item)) {
std::cout << " not";
}
std::cout << " found in array.";
}
std::cout << std::endl;
return 0;
}
Erläuterungen zu ergänzen.
Programm 4.30
C++: Testtreiber zu
Indexbereich mit
maximaler Summe in
gleitpunktzahligen
Reihungen
// Implementation file: TesterOfTSADDetermine.cpp
#include <cstdlib>
#include <iostream>
#include "ToolsetArrayComparableNumericM.hpp"
#include "TesterOfTSAC.hpp"
namespace tsac = ToolsetArrayComparable;
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
4 – 46
4 Reihungen und Abstraktionen
int main (int argc, char * argv[]) {
if (argc < 3 || std::atoi(argv[1]) <= 0 || std::atof(argv[2]) > std::atof(argv[3])) {
std::cout << "Please call this command with four integer arguments"
<< " size (> 0), min, max (>= min), item!";
} else {
int size = std::atoi(argv[1]);
double min = std::atof(argv[2]);
double max = std::atof(argv[3]);
TesterOfTSAC<double> tester = TesterOfTSAC<double>(size, min, max);
int max_low,
max_high;
double max_sum;
tsac::determine_range_with_max_sum
(tester.get_array(), max_low, max_high, max_sum);
std::cout << "max_low: " << max_low << " max_high: " << max_high
<< " max_sum: " << max_sum;
}
std::cout << std::endl;
return 0;
}
Erläuterungen zu ergänzen.
Da die Klasse InitializerOfArray für unsere Beispiele ausreicht, verzichten wir hier auf
Ableitungsklassen.
Aufgabe 4.15
C++: Abstrakte und
konkrete Initialisierer
und Testtreiber
Schreiben Sie ausgehend von Programm 4.27 drei Initialisiererklassen in C++, die dem
Entwurf Bild 4.3 und den Programmen 4.19, 4.20 und 4.21 entsprechen und erweitern
Sie den generischen Testtreiber Programm 4.28 dem Entwurf Bild 4.4 entsprechend,
aber nur soweit nötig, zu abstrakten und konkreten Testtreibern!
Aufgabe 4.16
C++: Testtreiber zu
Klassenvarianten der
Werkzeugkästen
Schreiben Sie C++-Varianten der Programme 4.29 und 4.30, die die Klassenvarianten
der Werkzeugkästen und benutzen!
4.4.3
Java
Aufgabe 4.17
Java: Testtreiber mit
wiederverwendbaren
Teilen
Entwickeln Sie eine Java-Variante zu dieser Problemlösung!
4.4.4
C#
Aufgabe 4.18
C#: Testtreiber mit
wiederverwendbaren
Teilen
Entwickeln Sie eine C#-Variante zu dieser Problemlösung!
4.4.5
Fazit
Zu ergänzen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5
Programmeinheiten und -strukturen
AufgabeBeispiel
5
Bild
5 5 (5)
Leitlinie Programm
5
5
Tabelle 5
Die folgenden Kapitel untersuchen programmiersprachliche Konzepte und ihre Realisierungen in programmiersprachlichen Konstrukten. Die Vorgehensweise ist umgekehrt zu gängigen Erstsemester-Programmierkursen: Wir beginnen mit Konzepten, die
für den Entwurf und die Spezifikation von Software im Großen wesentlich sind, und
behandeln weitere softwaretechnisch relevante Konzepte vor speziellen unwesentlichen Details einzelner Sprachen.
Wozu braucht man große Programmeinheiten? Dafür sprechen folgende softwaretechnische, miteinander verwobene Aspekte:
Zerlegung, Modularisierung eines Systems, um seine Komplexität zu reduzieren
programmiersprachliches Konstrukt, auf das sich große logische Einheiten aus Analyse und Entwurf abbilden lassen
arbeitsteilige Entwicklung von Teilsystemen
physische Einheit zum Speichern, Übersetzen, Binden, Laden von Programmteilen
Kapselung von Programmteilen, Bildung von Namensräumen, um Namenskonflikte
zu reduzieren
Geheimnisprinzip, Datenabstraktion
Trennung von Schnittstelle und Implementation
Austauschbarkeit
Wiederverwendbarkeit, Bibliotheken
Erweiterbarkeit
5.1
Kompilationsarten und -einheiten
Kompilation eines vollständigen Programms an einem Stück ist praktisch nicht brauchbar. Effiziente arbeitsteilige Entwicklung und Pflege sowie Austauschbarkeit und Wiederverwendbarkeit von Software erfordern die Möglichkeit, Programmteile einzeln zu
kompilieren. Dazu gibt es verschiedene, historisch nacheinander entwickelte Arten:
Unabhängige
Kompilation
Getrennte Kompilation
Bei unabhängiger Kompilation wird ein Programmteil ohne Kenntnis der anderen
Teile des Gesamtprogramms kompiliert. Erst beim Binden der einzelnen Teile sind
offene Referenzen feststellbar. Typfehler werden u.U. nicht einmal vom Laufzeitsystem erkannt. Dieser Mechanismus ist für Kompilierer und Binder einfach, aber
für Programmierende fehleranfällig.
Getrennte Kompilation bedeutet, dass zur Kompilationszeit alle verwendeten Größen bekannt sind und Typprüfungen über die Grenzen des kompilierten Teils hinaus
durchgeführt werden – so, als ob das Programm als Ganzes kompiliert wird. Dazu
müssen die Kompilationseinheiten genau definierte Schnittstellen besitzen. Der
Kompilierer überprüft, ob eine Kundeneinheit sich an die Schnittstelle einer benutzten Lieferanteneinheit hält (z.B. zeitliche Reihenfolge der Kompilation, Parameteranzahl, Typverträglichkeit). Dieser Mechanismus ist für Kompilierer und Binder
aufwändiger, bietet aber mehr Sicherheit gegen Programmierfehler.
Oft ist von getrennter Kompilation die Rede, wo tatsächlich nur unabhängige Kompilation vorliegt.
Speicherungs- und
Kompilationseinheit
Die Speicherungseinheit für Programmteile ist meist eine Datei, wie sie das Dateisystem des Betriebssystems bereitstellt. Zu unterscheiden sind Quellprogrammdateien
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 5 – Seite 1 von 18
5–2
5 Programmeinheiten und -strukturen
(source file) und Objektcodedateien (object file). Zu ihrer Unterscheidung kann ein
Kompilierer bestimmte Dateinamenserweiterungen und Verzeichnisstrukturen fordern.
Eine Kompilationseinheit ist eine syntaktische Einheit der Sprache, die der Kompilierer übersetzen kann. Oft entspricht einer Kompilationseinheit das Startsymbol der Syntaxregeln. Zwischen Kompilations- und Speicherungseinheiten sind u.A. folgende
Beziehungen möglich:
(1)
k:q:o: Beim orthogonalen Ansatz können k Kompilationseinheiten auf q Quelldateien verteilt sein, aus denen o Objektcodedateien erzeugt werden.
k:1:k: Eine Quelldatei kann k Kompilationseinheiten enthalten, aus denen k
Objektcodedateien erzeugt werden.
k:1:1: Eine Quelldatei kann k Kompilationseinheiten enthalten, aus denen eine
Objektcodedatei erzeugt wird.
1:q:1: Eine Kompilationseinheit kann auf q Quelldateien verteilt sein, aus denen
eine Objektcodedatei erzeugt wird.
1:1:o: Eine Quelldatei enthält eine Kompilationseinheit, aus denen o Objektcodedateien erzeugt werden.
1:1:1: Jede Kompilationseinheit ist in genau einer Quelldatei enthalten, die sonst
nichts enthält und aus der genau eine Objektcodedatei erzeugt wird.
(2)
(3)
(4)
(5)
(6)
(1) ist die flexibelste, aber auch komplexeste Option, während (6) die unflexibelste,
aber einfachste Option darstellt. Bei (6) können eine Kompilationseinheit und die zugehörigen Dateien fast identische, eindeutig aufeinander abbildbare Namen haben, was
die Organistation und Handhabbarkeit vereinfacht. Bei den anderen Optionen sind
Namen von Kompilationseinheiten und Dateien zu unterscheiden. Vor- und Nachteile
der Optionen sind gegeneinander abzuwägen, um zu entscheiden, ob sich der Mehraufwand für eine der Optionen (1) bis (5) lohnt.
Entwurfsaspekte
Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen:
Soll der Kompilierer unabhängig oder getrennt kompilieren?
Was ist eine Kompilationseinheit?
Wie sind Kompilations- und Speicherungseinheiten einander zugeordnet?
Tabelle 5.1 Physische Einheiten
Eigenschaft
Component
Pascal
Kompilationseinheit zu Dateien
Eiffel
C++
1:1:1
Kompilationseinheit
Klasse
Java
1:q:1
1:1:o
fast beliebige
Programmteile
mehrere Klassen und
Schnittstellen
Modul
Ladeeinheit
Kompilationsart
5.1.1
System
getrennt
C#
unabhängig
Klasse
System oder
einzelne Klassen
und Methoden
einer Ansammlung
getrennt
Component Pascal
Module sind die einzigen Kompilationseinheiten, sie werden getrennt am Stück in
Objektcode kompiliert. Component Pascal ist so entworfen, dass trotz dynamischen
Ladens von Modulen keine Schnittstellenverträglichkeitsfehler zur Laufzeit vorkom-
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5.1 Kompilationsarten und -einheiten
5–3
men. Zu jedem Modul gibt es i.d.R. eine Quelltextdatei. Der Kompilierer erzeugt aus
dem Quelltext eine Schnittstellen- und eine Objektcodedatei. Durch die IMPORTAbschnitte kennt der Kompilierer die benutzten Module. Per Namenskonvention findet
er ihre Schnittstellendateien und kann so die Einhaltung der Schnittstellen zwischen
den Modulen prüfen. Die IMPORT-Beziehung ist zyklenfrei.
☺ Da der Kompilierer die Schnittstellendateien automatisch erzeugt, ist die Konsis-
tenz zwischen Objektcode und Schnittstelle stets garantiert.
☺ Benutzte Schnittstellen liegen kompiliert vor. Das verkürzt Kompilationszeiten.
☺ IMPORT-Ketten bilden kein Problem, da der Kompilierer sie auflöst.
☺ Wird die Schnittstelle eines Moduls erweitert oder seine Implementation geändert,
so brauchen die Kunden dieses Moduls nicht nachkompiliert werden. Nachkompilieren ist erst erforderlich, wenn Schnittstellen benutzter Dienste geändert wurden.
BlackBox enthält kein Werkzeug für automatisches Nachkompilieren.
5.1.2
Eiffel
In Eiffel sind Klassen die einzigen Kompilationseinheiten, sie werden getrennt am
Stück in Objektcode kompiliert. Zu jeder Klasse gibt es eine gleichnamige Datei mit
der Erweiterung „.e“, die genau ihren Quelltext enthält. Durch die typisierten Vereinbarungen in der Klasse kennt der Kompilierer die benutzten Klassen. Per Namenskonvention findet er ihre Dateien und kann dadurch die Einhaltung der Schnittstellen (Typverträglichkeit) prüfen.
☺ Eiffel-Entwicklungsumgebungen sorgen meist für automatisches Nachübersetzen
und damit dafür, dass die übersetzten Einheiten konsistent zusammenpassen.
☺ Benutzungsketten bilden kein Problem, da der Kompilierer sie auflöst.
Erweiterungen von Schnittstellen und Änderungen an Implementationen führen zu
Nachkompilierungen von Kunden, da der Kompilierer nur erkennen kann, dass der
Inhalt einer Lieferantendatei geändert wurde, aber nicht die Art der Änderung.
Manche Sprachmerkmale wie kovariante Vererbung erfordern eine globale Analyse
der Quelltexte. Eiffel würde daher dynamisches Laden von Komponenten nur auf
Kosten von Sicherheit oder mehr Laufzeitprüfungen erlauben. Globale Analyse verlängert Kompilationszeiten. Verzichtet der Kompilierer auf globale Analyse, so sind
Typfehler zur Laufzeit möglich.
5.1.3
C++
In C++ sind Dateien ziemlich beliebigen Inhalts (die einzigen) Kompilationseinheiten,
sie werden unabhängig am Stück in Objektcode kompiliert. Danach bindet der Binder
die einzelnen Codestücke zusammen, dabei löst er externe Referenzen auf. Er stellt
doppelt definierte Namen und offene Referenzen fest, kann aber keine Parameteranzahl- und -typprüfungen übernehmen, da ihm die dazu erforderlichen Informationen
fehlen.
Die unabhängige Kompilation in C++ folgt aus dem Entwurfsziel der Kompatibilität
mit C und der Zusammenarbeit mit der relativ primitiven Umgebung des C-Binders.
Als Konsequenz kann der Kompilierer nicht garantieren, dass Größen eindeutig definiert, Informationen (z.B. über Typen) konsistent oder Größen initialisiert sind, bevor
sie benutzt werden. Typsicherheit ist nicht garantiert.
C++ bietet wie C mit Vorübersetzerdirektiven Mittel, die – wenn sie diszipliniert
benutzt werden – näherungsweise getrennte Kompilation ermöglichen, um der Fehleranfälligkeit unabhängiger Kompilation entgegenzuwirken. Mit Inkludierung sind
nämlich mehrere Dateien gemeinsam kompilierbar.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5–4
5 Programmeinheiten und -strukturen
Im einfachsten Fall schreibt der Programmierer zu jeder Kompilationseinheit (Implementationsdatei, source file, Namenskonvention: Dateiname.c oder .cpp) eine Schnittstellendatei (header file, Namenskonvention: Dateiname.hpp). Eine Kundendatei inkludiert die Schnittstellendatei eines Lieferanten. Durch den Effekt der #include-Direktive
kennt der Kompilierer den Inhalt der Schnittstellendatei und kann dadurch die Einhaltung der Schnittstelle zwischen Kunden- und Lieferantendatei überprüfen. Die Inkludierungsbeziehung muss zyklenfrei sein.
Der Programmierer ist für die Konsistenz zwischen Implementations- und Schnittstellendatei verantwortlich. Häufige Fehlerquelle bleibt die Inkludierung falscher
oder nicht aktualisierter Schnittstellendateien.
Bei jeder Inkludierung wird eine Schnittstellendatei neu kompiliert. Das verlängert
Kompilationszeiten.
Wird durch Inkludierungsketten dieselbe Schnittstellendatei mehrfach inkludiert, so
findet der Kompilierer mehrfach vorkommende Namen, produziert also Kontextfehlermeldungen. Programmierkonventionen mit Vorübersetzerdirektiven (#ifndef,
#define, #endif) lösen dieses Problem.
Inhalt der Schnittstellendatei xmp.hpp:
Beispiel
#ifndef XMP_HPP_
#define XMP_HPP_
#include "other.hpp"
Definitionen von Konstanten und Typen
Externdeklarationen exportierter Funktionen
#endif // XMP_HPP_
Die Schnittstellendatei ist keine Schnittstellendatei im strikten Wortsinn, da sie
nicht nur Schnittstelleninformationen enthält, sondern auch Implementationsteile.
Im Falle generischer Einheiten enthält sie die vollständige Implementation.
Werkzeuge wie make, die Nachkompilationen gemäß Abhängigkeiten zwischen
Kompilationseinheiten steuern, entscheiden oft aufgrund der Zeitstempel der
Dateien. Das führt bei Schnittstellenerweiterungen und Implementationsänderungen
zu unnötigen Nachkompilationen von Kunden, die diese Erweiterungen bzw. Änderungen gar nicht betreffen.
5.1.4
Java
In Java sind Kompilationseinheiten Ansammlungen von Klassen und Schnittstellen, sie
werden getrennt am Stück in Bytecode kompiliert. Zu jeder Kompilationseinheit gibt
es eine Quelldatei, die denselben Namen wie die erste Klasse der Einheit trägt mit der
Erweiterung „.java“. Zu jeder Klasse gibt es eine gleichnamige Bytecodedatei mit der
Erweiterung „.class“. Durch die typisierten Vereinbarungen in der Kompilationseinheit kennt der Kompilierer die benutzten Klassen. Per Namenskonvention findet er ihre
Dateien und kann dadurch die Einhaltung der Schnittstellen prüfen.
Aufgabe 5.1
Java: Getrennte
Kompilation
Finden Sie heraus, welche Eigenschaften getrennte Kompilation bei Java hat!
5.1.5
C#
Zu ergänzen.
Aufgabe 5.2
C#: Getrennte
Kompilation
Finden Sie heraus, welche Eigenschaften getrennte Kompilation bei C# hat!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5.2 Zusammenfassung und Kapselung von Programmteilen
5–5
5.2
Zusammenfassung und Kapselung von Programmteilen
Kapselung
Kapselung (encapsulation) bedeutet, Daten, Operationen und andere Programmteile
zu einer Einheit – einer Kapsel – zusammenzufassen, die i.d.R. ein Sprachkonstrukt mit
einem Namen ist. Gekapselte Teile sind i.d.R. nur mit qualifizierten Namen zugreifbar.
Kapselung wird oft zusammen mit Geheimnisprinzip (information hiding) und Datenabstraktion (data abstraction) genannt und manchmal damit verwechselt. Tatsächlich
folgt jedoch aus dem Kapseln noch kein Verstecken von Programmteilen.
Kapselung kleiner
Programmteile
Bei kleinen Programmteilen unterscheiden wir die Kapselung von Algorithmen und die
Kapselung von Daten:
Algorithmenkapselung ist lang bekannt: Routinen (Prozeduren, Funktionen) z.B.
in Fortran 1957, Koroutinen z.B. in Simula 67, Prozesse in Concurrent Pascal
1974.
Datenkapselung ist in Form von Verbunden (record) in Cobol seit 1960 bekannt.
Tabelle 5.2 Kapselung kleiner und großer Programmteile
Eigenschaft
Component Pascal
Eiffel
Algorithmenkapselung
Prozedur
Routine
Verbund
Datenkapselung
Modul
-
C++
Java
C#
Funktion
Verbund
Datei
-
ADT
Paket
Namensraum
Klasse
Modul
Typenkapselung
Kapselung großer
Programmteile
Entwurfsaspekte
sprachextern:
Subsystem
sprachextern:
Cluster
Namensraum
sprachextern:
Ansammlung
Auf die kombinierte Kapselung von Daten und Algorithmen mit Modulen und Klassen
gehen wir in 5.4 und 6 ein. Die softwaretechnisch interessante Frage ist hier, ob und
welche Sprachkonstrukte es gibt, mit denen man große Programmteile, also etwa viele
Module oder Klassen, zu größeren Einheiten zusammenfassen kann. Gründe dafür
sind:
Kapselung hilft, bei großen Systemen große Analyse- und Entwurfseinheiten abzubilden.
Ohne Kapselungsmöglichkeit treten unvermeidlich Namenskonflikte auf, wenn
unabhängig voneinander entwickelte Teilsysteme zu einem System integriert werden sollen.
Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen:
Soll das Problem außerhalb der Sprache durch Zusammenfassungen von Programmteilen oder innerhalb der Sprache durch ein Kapselungskonstrukt behandelt
werden?
Soll dafür ein schon vorhandenes Kapselungskonstrukt verwendet oder ein zusätzliches eingeführt werden?
Soll das Kapselungskonstrukt nur Sichtbarkeitsbereiche oder auch Zugriffsrechte
definieren?
5.2.1
Component Pascal
Component Pascal bietet keine größeren Kapseln als Module. Ein System kann leicht
hunderte Module umfassen. Vereinigt man zwei Systeme, so sind Konflikte bei Modul-
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5–6
5 Programmeinheiten und -strukturen
namen nicht ausgeschlossen. Component Pascal delegiert die Lösung dieses Problems
an seine Umgebung.
BlackBox arbeitet mit Subsystemen. Ein Subsystem ist eine Menge von Modulen. Subsysteme zerlegen ein System in disjunkte Teilmengen von Modulen. Jedes Subsystem
hat einen Namen, der bei Oberon microsystems registriert werden kann und dann weltweit eindeutig ist. Vereinigt man zwei in Subsysteme zerlegte Systeme, so sind
Namenskonflikte bei registrierten Subsystemen ausgeschlossen, bei nicht registrierten
Subsystemen unwahrscheinlich.
Die Zuordnung von Modulen zu Subsystemen erfolgt durch eine einfache Namenskonvention. Der Subsystemname ist Teil des Modulnamens. Ein Modul einem anderen
Subsystem zuordnen erfordert eine Änderung des Modulnamens und damit des Quelltexts des Moduls. Jedem Subsystem ist ein Dateiverzeichnis zugeordnet, das alle
Module des Subsystems enthält. Da Subsysteme kein Teil der Sprache sind, können sie
keine eigenen Sichtbarkeitsbereiche oder Zugriffsrechte definieren.
5.2.2
Eiffel
Eiffel bietet keine größeren Kapseln als Klassen. Ein System kann leicht viele hunderte
Klassen umfassen. Vereinigt man zwei Systeme, so sind Konflikte bei Klassennamen
wahrscheinlich. Eiffel delegiert die Lösung dieses Problems an seine Umgebung.
ISE Eiffel arbeitet mit Clustern. Ein Cluster ist eine Menge von typischerweise 5 bis 40
Klassen. Cluster zerlegen ein System in disjunkte Teilmengen von Klassen. Jedem
Cluster ist ein Dateiverzeichnis zugeordnet, das alle Klassen des Clusters enthält.
Namenskonflikte sind trotz der Cluster nicht ausgeschlossen; sie werden durch lokales
Umbenennen konfligierender Klassen in einer Ace-Datei gelöst, der Quelltext der Klassen bleibt davon unberührt. Ace steht für Assembly of Classes in Eiffel. Eine Ace-Datei
wird in Lace (Language for the Ace) geschrieben und steuert die Kompilation eines
Systems.
Beispiel 5.1
Eiffel: Ace-Datei1
system
example
root
CALCULATOR (my_cluster1): "make"
default
assertion (ensure);
precompiled ("$EIFFEL3|precomp|spec|$PLATFORM|base");
debug (no)
cluster
my_cluster1: "mydir|project1|subdir";
her_cluster2: "herdir|project2|subdir1|subdir2"
adapt
my_cluster1:
rename STACK as MY_STACK;
end
Da Cluster kein Teil der Sprache sind, können sie keine eigenen Sichtbarkeitsbereiche
oder Zugriffsrechte definieren.
5.2.3
C++
C++ bietet mit Namensräumen größere Kapseln als Klassen und Dateien. Ein
Namensraum (namespace) umfasst eine Menge von Vereinbarungen.
1
http://burks.brighton.ac.uk/burks/language/eiffel/oveiffel/system.htm (Zugriff 2004-10-01).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5.2 Zusammenfassung und Kapselung von Programmteilen
5–7
namespace Xmp {
Vereinbarungen
} // end of Xmp
Namensräume
wurden in C++ eingeführt, um das C-Problem des globalen Namensraums zu lösen,
haben einen Namen, der kundenseitig Aliasnamen erhalten kann,
definieren einen Sichtbarkeitsbereich durch einen zugehörigen Block, aber keinen
Schutz und keine Zugriffsrechte,
liegen orthogonal zu Dateien, d.h. ein Namensraum kann sich über mehrere Dateien
erstrecken und eine Datei kann Teile mehrerer Namensräume enthalten,
können textuell geschachtelt werden,
erlauben es z.B., Module und Subsysteme von Component Pascal nachzubilden
(siehe 5.4).
Programm 5.1
C++: using-Direktive
#include <iostream>
using namespace std;
int main (int argc, char * argv[]) {
cout << "Hello World" << endl;
return 0;
}
Beseitigter
Namensraum
Bei dieser leider oft im Web zu findenden Hello-World-Variante sorgt die using-Direktive dafür, dass die Namen cout und endl aus dem Standardnamensraum std unqualifiziert verwendbar sind; die qualifizierten Namen lauten bekanntlich std::cout und
std::endl.
„It is generally in poor taste to dump every name from a namespace into the global
namespace“ [Str97] S. 47.
Stroustrup hat using als Migrationsmittel vorgesehen, das ermöglicht, Programmteile
einzubinden, die vor der Einführung von Namensräumen erstellt wurden [Str97]
S. 172, 183. Freilich sind so Missbräuche von using nicht zu verhindern; Programmierrichtlinien können helfen. Was ist problematisch an using?
Namensräume und qualifizierte Zugriffe sollen
die in einem globalen Namensraum auftretenden Namenskonflikte vermeiden
helfen,
die Lesbarkeit einzelner Kompilationseinheiten erhöhen, da ein qualifizierter
Name den Ort seiner Definition anzeigt.
Das Entqualifizieren von Namen widerspricht diesen softwaretechnischen Zielen.
Entqualifizierte Namen können zu Überladungen führen, die unbeabsichtigte
Effekte bewirken. Solche Fehler sind schwer zu lokalisieren, da alle gleichen
Namen in allen mit using benutzten Namensräumen involviert sein können.
Leitlinie 5.1
C++: Vermeide using
27.9.12
Vermeide, using-Direktiven zu verwenden, da sie die Lesbarkeit mindern und zu
Namenskonflikten und unbeabsichtigten Überladungen führen können! Verwende
using nur, um alte Programmteile unverändert zu benutzen! Verwende namespaceAlias-Definitionen statt using!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5–8
Programm 5.2
C++: Namensraum mit
Aliasnamen
5.2.4
5 Programmeinheiten und -strukturen
#include <iostream>
namespace s = std;
int main (int argc, char * argv[]) {
s::cout << "Hello World" << s::endl;
return 0;
}
Java
Java bietet mit Paketen größere Kapseln als Klassen. Ein Paket (package) besteht aus
mehreren Kompilationseinheiten und enthält Unterpakete, Klassen und Schnittstellen.
package Xmp;
Importvereinbarungen
Typenvereinbarungen
Pakete
sind Teil der Sprache,
haben einen Namen,
definieren einen Sichtbarkeitsbereich durch eine Paketvereinbarung, die für die
Kompilationseinheit gilt, und Zugriffsrechte,
können sich über mehrere Kompilationseinheiten erstrecken,
können logisch geschachtelt werden.
Java-Pakete unterscheiden sich von C++-Namensräumen wesentlich darin, dass sie
auch Schutzräume darstellen.
Das Analogon zur C++-using-Direktive ist die Java-import-Deklaration. Das softwaretechnisch fragwürdige import kann kaum als Migrationsmittel wie bei C++ konzipiert sein.
Leitlinie 5.2
Java: Vermeide import
5.2.5
Vermeide, import-Deklarationen zu verwenden, da sie die Lesbarkeit mindern und
zu Namenskonflikten und unbeabsichtigten Überladungen führen können!
C#
C# bietet mit Namensräumen ähnlich wie C++ größere Kapseln als Klassen. Ein
Namensraum umfasst eine Menge von Klassen.
namespace Xmp {
using-Direktiven
Typenvereinbarungen
} // end of Xmp
Namensräume
sind Teil der Sprache,
haben einen Namen,
definieren einen Sichtbarkeitsbereich,
liegen orthogonal zu Dateien,
können textuell oder logisch geschachtelt werden.
Weiter bietet C# Ansammlungen. Eine Ansammlung (assembly) ist eine Menge von
Dateien, die zusammen eine DLL (dynamic link library) oder ein EXE (executable)
ergeben. Eine Ansammlung hat höchstens einen Eintrittspunkt. Der Modifikator
internal zeigt an, was nur in der gegebenen Ansammlung sichtbar sein soll.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5.3 Geheimnisprinzip, Schutz und Zugriffsrechte
Programm 5.3
C#: using-Direktive
5–9
using System;
public class HelloWorld {
public static void Main (string[] args) {
Console.WriteLine("Hello World");
}
}
In Programm 5.3 muss der Name Console wegen der using-Direktive nicht voll qualifiziert werden. Das C#-Analogon zur C++-using-Direktive und Java-import-Deklaration ist softwaretechnisch genauso fragwürdig wie jene.
Leitlinie 5.3
C#: Vermeide using
5.3
Vermeide, using-Direktiven zu verwenden, da sie die Lesbarkeit mindern und zu
Namenskonflikten und unbeabsichtigten Überladungen führen können!
Geheimnisprinzip, Schutz und Zugriffsrechte
Tabelle 5.3 Schutz, Rechte und Verbote
Eigenschaft
Schutzeinheit
Component
Pascal
Eiffel
C++
Java
C#
Modul
Objekt
Datei
Klasse
Klasse
Paket
Klasse
Ansammlung
Zugriffsrechte außerhalb der Schutzeinheit spezifizierbar?
Datenelement
lesen-schreiben
ja
nein
Datenelement nur lesen
ja
ja
nein
Routine ausführen
ja
Routine nur redefinieren
ja
nein
Zugriffsverbote außerhalb der Schutzeinheit spezifizierbar?
Objekt vereinbaren
ja
Objekt erzeugen
Objekt zuweisen
ja
?
Weitere Rechte und Verbote gibt es im Zusammenhang mit Vererbung.
5.3.1
Component Pascal
MODULE Xmp;
TYPE
XmpClass* =
RECORD
xmpReadWrite
: INTEGER;
globalReadOnly- : REAL;
globalReadWrite* : CHAR;
END;
VAR
xmpReadWrite
globalReadOnlyglobalReadWrite*
27.9.12
: INTEGER;
: REAL;
: CHAR;
(* Meist schlechter Stil! *)
(* Meist schlechter Stil! *)
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5 – 10
5 Programmeinheiten und -strukturen
PROCEDURE XmpExec;
BEGIN
Anweisungen
END XmpExec;
PROCEDURE GlobalExec*;
VAR
loc : INTEGER;
BEGIN
Anweisungen
END GlobalExec;
PROCEDURE (VAR this : XmpClass) GlobalRedefineOnly-;
BEGIN
Anweisungen
END GlobalRedefineOnly;
END Xmp.
5.3.2
Eiffel
class XMP_CLASS
feature {NONE}
xmp_object_read_write : CHAR
feature {XMP_CLASS}
xmp_class_read_only : INTEGER
feature {OTHER_CLASS}
other_class_read_only : BOOLEAN
feature {ANY}
global_read_only : REAL
-- ANY ist Standard
feature
global_exec
local
loc : INTEGER
do
Anweisungen
end -- global_exec
end -- class XMP_CLASS
5.3.3
C++
class Xmp_class {
private:
int xmp_class_read_write;
// private ist hier Standard
protected:
int xmp_derived_class_read_write;
public:
char global_read_write;
// Meist schlechter Stil!
void global_exec () {
int loc;
Anweisungen
} // end of global_exec
}; // end of Xmp_class
static int file_read_write;
char global_read_write;
// Oft schlechter Stil!
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5.3 Geheimnisprinzip, Schutz und Zugriffsrechte
5 – 11
static int file_exec () {
int loc;
Anweisungen
}; // end of file_exec
void global_exec () {
int loc;
Anweisungen
}; // end of global_exec
5.3.4
Java
package Xmp;
class PrivClass {
private int privClassReadWrite;
protected int xmpExtendedPrivClassReadWrite;
public char xmpReadWrite;
// Meist schlechter Stil!
public void xmpExec () {
int loc;
Anweisungen
} // end of xmpExec
bool xmpReadWrite;
}; // end of XmpPrivateClass
public class PubClass {
private int pubClassReadWrite;
protected int extendedPubClassReadWrite;
public char globalReadWrite;
// Meist schlechter Stil!
public void globalExec () {
int loc;
Anweisungen
} // end of globalExec
boolean xmpReadWrite;
}; // end of XmpPubClass
5.3.5
C#
namespace Xmp {
class PrivClass {
private int privClassReadWrite;
protected int xmpExtendedPrivClassReadWrite;
public char xmpReadWrite;
// Meist schlechter Stil!
public void xmpExec () {
int loc;
Anweisungen
} // end of xmpExec
bool xmpReadWrite;
}; // end of XmpPrivClass
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5 – 12
5 Programmeinheiten und -strukturen
public class PubClass {
private int pubClassReadWrite;
protected int extendedPubClassReadWrite;
public char globalReadWrite;
// Meist schlechter Stil!
public void globalExec () {
int loc;
Anweisungen
} // end of globalExec
bool xmpReadWrite;
}; // end of XmpPubClass
}; // end of Xmp
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Component Pascal
Syntax
Eiffel
Exportmarke an jedem Merkmal
C++
Abschnitt für mehrere Merkmale
Java
C#
Modifikator bei jedem Merkmal
Semantik
Zugriffsberechtigte
Zugriffsrechte
alle Klassen/
alles in der Umgebung
alle Klassen im selben
Paket/Programm und
alle Erweiterungsklassen
„*“
☺
Datenelement
nur lesen
„-“
feature
Routine ausführen
„*“
feature {ANY}
nur-lesen,
ausführen
public
feature {CLIENT1,
CLIENT2,...}
☺
protected
protected
internal
kein
Modifikator
internal
protected,
jede Klasse in
eigenem Paket
protected
alle
alles im selben Modul/
Paket/Programm
nur
Erweiterungsklassen
public
keine Exportmarke
Routine
nur redefinieren
„-“
feature {NONE}
lesen-schreiben,
ausführen
nur Klasse selbst
Legende: Ein ☺,
oder
feature {}
keine Exportmarke,
Klasse allein in eigenem Modul
☺
protected
private
oder nichts
private
bedeutet „nicht möglich“.
5 – 13
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
durch Liste spezifizierte
Kundenklassen
Datenelement
lesen-schreiben
5.3 Geheimnisprinzip, Schutz und Zugriffsrechte
27.9.12
Tabelle 5.4 Schutz und Zugriffsrechte bei Klassen
5 – 14
5.4
5 Programmeinheiten und -strukturen
Module und Datenabstraktion
Ein Modul ist eine Programmeinheit, die Daten und Routinen zu einer bezeichneten
Einheit zusammenfasst (kapselt) und eine Schnittstelle für externe Zugriffe festlegt.
Module sind Bausteine modularer Programme. Datenabstraktion bedeutet das Verbergen der internen Struktur gekapselter Daten hinter einer für externe Zugriffe bereitgestellten Schnittstelle. Eine abstrakte Datenstruktur (ADS) ist ausschließlich durch ihr
von außen beobachtbares Verhalten definiert. Sie ist spezifiziert durch eine Menge
anwendbarer Operationen, wobei jede Operation durch ihre Signatur, ihre Aufrufkonvention und ihren Effekt beschrieben ist. Module sind oft ADSen.
Leitlinie 5.4
Module als ADSen
Entwerfe Module als abstrakte Datenstrukturen! Verbiete externe Schreibzugriffe
auf Daten von Modulen!
Ein abstrakter Datentyp (ADT) kombiniert Eigenschaften von ADSen und von
Typen. Von einem ADT können beliebig viele Exemplare existieren, die alle ADSen
sind. Eine Klasse ist ein erweiterbarer ADT, dessen Exemplare Objekte heißen.
Da alle fünf Sprachen ein Klassenkonstrukt bieten, lässt sich in jeder ein Modul auch
mit einer Klasse realisieren. Nach dem bekannten Singleton-Entwurfsmuster ist eine
Klasse zu definieren und ein einzelnes Objekt dieser Klasse zu vereinbaren und ggf. zu
erzeugen, das als gewünschtes Modul fungiert. Dies ist allerdings relativ aufwändig
und erfordert zusätzliche Maßnahmen, um das Modul-Objekt an gewünschten Stellen
sichtbar zu machen und zu verhindern, dass weitere Objekte der Klasse erzeugt werden.
Entwurfsaspekte
Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen:
Soll es ein eigenes Sprachkonstrukt für Module geben?
Falls ja, wie verhalten sich Module und Klassen zueinander?
Falls nein, soll das Klassenkonstrukt Module als Singleton-Objekte oder anders
unterstützen?
Im Folgenden betrachten wir ein abstraktes Beispiel mit zwei Modulen in einer Kunden-Lieferanten-Beziehung. Wir nutzen es als Vorlage, um mögliche Realisierungen
von Modulen in den verschiedenen Sprachen zu demonstrieren.
Beispiel 5.2
Modulare
Benutzungsbeziehung
5.4.1
Client
Supplier
Component Pascal
Component Pascal hat ein Sprachkonstrukt MODULE.
Programm 5.4
Component Pascal:
Kunden-LieferantenModule
MODULE I3Supplier;
TYPE
T* = RECORD ... END;
PROCEDURE Do* (IN t : T);
BEGIN
...
END Do;
END I3Supplier.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5.4 Module und Datenabstraktion
5 – 15
MODULE I3Client;
IMPORT
S := I3Supplier;
PROCEDURE Do* (IN t : S.T);
BEGIN
S.Do (t)
END Do;
END I3Client.
Abstrakte Datenstrukturen ermöglicht Component Pascal durch das Modulkonstrukt.
Es ist darauf zu achten, dass keine Variablen exportiert werden (höchstens nur lesbar).
5.4.2
Eiffel
Eiffel hat kein Sprachkonstrukt für Module. Unterstützung für abstrakte Datenstrukturen gibt es nur in Form von Exemplaren von Klassen, siehe nächsten Abschnitt. Zum
Sichtbarmachen von Modul-Objekten benutzt man Einmalfunktionen und mehrfaches
Erben (Mix-Ins).
5.4.3
C++
C++ hat kein Sprachkonstrukt für Module oder abstrakte Datenstrukturen. Sie sind
neben der oben genannten Möglichkeit von Singleton-Objekten auf zwei Arten mit drei
Sprachelementen realisierbar.
5.4.3.1
Prämodule
Das erste Sprachelement stammt von C und realisiert Prämodule mit Schnittstellenund Implementationsdateien. Ein Prämodul fasst Daten und Operationen zu einer Einheit zusammen und legt eine Schnittstelle für externe Zugriffe fest, hat aber keinen
Namen. Die in einem Prämodul vereinbarten Namen sind global, nicht gekapselt. Prämodule lassen sich in C/C++ durch zwei (oder mehr) Dateien realisieren:
Schnittstellendatei
Implementationsdatei
Eine Schnittstellendatei (header file) enthält
Definitionen von Konstanten,
Definitionen von Typen,
Externdeklarationen exportierter Variablen (schlechter Programmierstil),
Externdeklarationen exportierter Funktionen.
Eine Implementationsdatei (source file) enthält
Definitionen nicht exportierter Konstanten, Typen und Funktionen,
Definitionen nicht exportierter Variablen,
Definitionen exportierter Variablen (schlechter Programmierstil),
Definitionen exportierter Funktionen.
Man beachte, dass eine Schnittstellendatei keine Definitionen enthalten darf, die Speicherplatz beanspruchen! Nicht die Sprache erzwingt eine disziplinierte Vorgehensweise, sondern der Programmierer ist selbst dafür verantwortlich.
5.4.3.2
Namensräume
Das zweite Sprachelement sind die Namensräume von C++. Sie kapseln zwar Namen,
bilden aber keine Schutzeinheiten, d.h. eignen sich nicht dazu, Schnittstellen festzulegen. Kombiniert man jedoch die Möglichkeiten von Dateien und Namensräumen, so
lassen sich Module realisieren. Programm 5.5 bietet etwas mehr als Programm 5.4, da
I3 hier als Namensraum einen eigenen Sichtbarkeitsbereich definiert, während I3 in
Programm 5.4 nur ein Subsystem ist.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5 – 16
Programm 5.5
C++: KundenLieferanten-Module
5 Programmeinheiten und -strukturen
Inhalt der Schnittstellendatei C++/I3/Include/supplier.hpp:
// Header file: supplier.hpp:
#ifndef I3_SUPPLIER_HPP_
#define I3_SUPPLIER_HPP_
namespace I3 {
namespace Supplier {
class T {...};
extern void do (const T & t);
}
}
#endif // I3_SUPPLIER_HPP_
Inhalt der Implementationsdatei C++/I3/Source/supplier.cpp:
// Implementation file: supplier.cpp:
#include "supplier.hpp"
namespace I3 {
namespace Supplier {
void do (const T & t) {
...
}
}
}
Inhalt der Schnittstellendatei C++/I3/Include/client.hpp:
// Header file: client.hpp:
#include "supplier.hpp"
#ifndef I3_CLIENT_HPP_
#define I3_CLIENT_HPP_
namespace I3 {
namespace Client {
namespace S = I3::Supplier;
extern void do (const S.T & t);
}
}
#endif // I3_CLIENT_HPP_
Inhalt der Implementationsdatei C++/I3/Source/client.cpp:
// Implementation file: client.cpp:
#include "supplier.hpp"
#include "client.hpp"
namespace I3 {
namespace Client {
namespace S = I3::Supplier;
void do (const S.T & t) {
S.do(t);
}
}
}
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
5.4 Module und Datenabstraktion
5.4.3.3
5 – 17
Klassenmethoden
Eine andere Realisierung von Modulen in C++, die „programming more enjoyable for
the serious programmer“ macht, sind Klassen mit static-Merkmalen, die nicht nur bei
ihrer Definition, sondern auch bei Aufrufen an die Klasse gebunden sind, siehe 6.4 S.
6-7.
5.4.4
Java, C#
Java und C# übernehmen von C++ das Sprachkonstrukt der static-Merkmale und die
Möglichkeit, damit Module zu realisieren, siehe 6.4 S. 6-7.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5 – 18
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
5 Programmeinheiten und -strukturen
27.9.12
6
Klassen und objektorientierte Konzepte
Aufgabe
Beispiel
6
Bild
6 waren
6 (6) unsere
Leitlinie
Programm
6
6
Tabelle
6
Traditionell
Programmiersprachen
dominiert
von den Maschinen, auf
denen sie ausführbar sein sollten. Doch in dem Maße, in dem unsere Fähigkeit
wuchs, Sprachkonstrukte in Maschinenkonzepte zu übersetzen, nahm auch unsere
Freiheit zu, die Sprachen an die Welten der Probleme anzupassen. So ist es heute
ein zentrales Anliegen bei der Entwicklung moderner Programmiersprachen, Situationen, wie sie in Anwendungen typisch auftreten, unmittelbar in Sprachkonzepten
widerzuspiegeln.
Peter Pepper1
6.1
Abstrakte Datentypen
Ein abstrakter Datentyp (ADT) kombiniert die Typeigenschaft mit der Eigenschaft
der ADS (s. 5.4 S. 5-14):
Von einem Typ können beliebig viele Exemplare vereinbart oder erzeugt werden.
Der Typ ist ein Bauplan für seine Exemplare.
Jedes Exemplar eines ADTs ist eine ADS mit eigenem Zustandsraum.
Ein ADT legt eine Schnittstelle und damit ein Verhalten fest. Alle Exemplare eines
ADTs übernehmen die Operationen ihres Typs, aber jedes Exemplar hat seine eigenen
Daten, auf denen die Operationen arbeiten.
Component Pascal
ADTen sind durch das Verbundkonstrukt RECORD ermöglicht. Es ist darauf zu achten,
dass keine Felder exportiert werden (höchstens nur lesbar).
Eiffel
ADTen sind durch das Klassenkonstrukt class gut unterstützt: Eine Klasse ist ein (ggf.
nur partiell) implementierter abstrakter Datentyp.
C++, C#
ADTen sind durch das Klassenkonstrukt class und das Verbundkonstrukt struct ermöglicht. Es ist darauf zu achten, dass keine Datenelemente öffentlich gemacht werden.
Java
ADTen sind durch das Klassenkonstrukt class ermöglicht. Es ist darauf zu achten,
dass keine Datenelemente öffentlich gemacht werden.
6.2
Klassen
Klassen sind Grundelemente objektorientierter Programme. Eine Klasse ist ein ADT,
der beerbbar und partiell implementiert ist. Wie ADTen vereinen Klassen Merkmale
von ADSen und Typen. Eine Klasse fasst Daten und Operationen zu einer bezeichneten
Einheit zusammen und legt Schnittstellen zu Kunden- und Unterklassen fest. Die
Schnittstellen sollen rein operational sein, d.h. konkrete Daten sollen hinter der Schnittstelle verborgen bleiben. Die Schnittstellenoperationen nennen wir auch Dienste (service). Es ist softwaretechnisch sinnvoll, Dienste zu teilen in Abfragen (query), die eine
Information liefern, aber nichts verändern, und Aktionen (action), die etwas verändern, aber keine Information liefern.
Die Implementation einer Klasse besteht aus geschützten Daten und den in den Operationen gekapselten Algorithmen. Exemplare von Klassen heißen Objekte. Jedes
Objekt hat eigene Exemplare der Daten, aber alle Objekte einer Klasse benutzen dieselben Operationen.
1
Peter Pepper: Grundlagen der Informatik. R. Oldenbourg Verlag, München Wien (1992)
S. 146 v. 355.
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 6 – Seite 1 von 22
6–2
6 Klassen und objektorientierte Konzepte
Die wichtigen Beziehung zwischen Klassen sind
die Benutzungs- (use) oder Kunde-Lieferant-Beziehung (client-supplier): Jede
Klasse ist Lieferant von Diensten, die von Kundenklassen benutzt werden können;
die Vererbungs- (inheritance) oder Oberklasse-Unterklasse-Beziehung (superclass-subclass): Jede Klasse kann als Oberklasse für von ihr erbende Unterklassen
dienen.
Im Vererbungsbegriff steckt eine genetische Metapher, die nicht durchweg passt. Angemessener scheint der neutralere Begriff der Erweiterung (extension), der sich jedoch
nicht durchgängig etabliert hat.
Component Pascal,
C++
Beide Sprachen sind hybride Sprachen, die objektorientiertes, aber auch modulares
bzw. prozedurales Programmieren ermöglichen.
Eiffel, Java, C#
Als rein objektorientierte Sprachen unterstützen Eiffel, Java und C# objektorientiertes
Programmieren. Das bedeutet nicht, dass jedes Programm in diesen Sprachen automatisch ein gutes objektorientiertes Programm ist. Schließlich kann man eine Problemlösung mit einer einzigen Klasse realisieren, die intern prozedural strukturiert ist. Dieser
Programmierstil heisst POOP (von procedural oder poor object-oriented programming).
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.2 Klassen
6–3
Tabelle 6.1 Bezeichnungsweisen
Component
Pascal
Eiffel
C++
Java
erweiterbarer Typ mit
typgebundenen Prozeduren
(extensible type with
type-bound procedures)
Klasse (class)
Variable eines
erweiterbaren...
(variable of an extensible...)
Objekt (object),
Exemplar einer Klasse (instance of a class)
C#
Typerweiterung
(type extension)
Vererbung
(inheritance)
Ableitung
(derivation)
Erweiterung
(extension)
Vererbung,
Ableitung
Basistyp (base type)
Vorfahre, Vorgänger
(parent, ancestor)
Basisklasse
(base class)
Oberklasse
(superclass)
Basisklasse
erweiterter Typ (extension)
Erbe, Nachfolger
(heir, descendant)
abgeleitete Klasse
(derived class)
Unterklasse
(subclass)
abgeleitete
Klasse
-
Merkmal (feature)
Feld (field)
Attribut (attribute)
(Daten-)Element
(data member)
Feld
typgebundene Prozedur,
Methode
(type-bound procedure)
Routine (routine)
Elementfunktion
(member function)
Methode (method)
gewöhnliche Prozedur
(proper procedure)
Prozedur (procedure)
Funktionsprozedur
(function procedure)
Element (member)
void-Funktion
(void function)
Funktion (function)
redefinieren (redefine)
abstrakte Klasse, Prozedur
(abstract class, procedure)
aufgeschobene Klasse,
Routine
(deferred class, routine)
-
generische Klasse
(generic class)
überschreiben (override)
abstrakte Klasse, Funktion
(abstract class, function)
Schablone
(template class)
generische Klasse
Component Pascal
Eine Klasse ist ein programmiererdefinierter Typ mit typgebundenen Prozeduren. Als
Konstrukt dient der konventionelle Verbund. Es gibt auch andere Typen, Variablen und
„normale“ Prozeduren, die nicht an einen Typ gebunden sind.
Eiffel, C#
Eine Klasse ist ein sprach- oder programmiererdefinierter Typ. Es gibt dafür ein spezielles Konstrukt. Es gibt keine Typen, die nicht von einer Klasse abgeleitet sind und
keine Merkmale, die nicht zu einer Klasse gehören.
C++
Eine Klasse ist ein programmiererdefinierter Typ. Es gibt dafür ein spezielles Konstrukt. Es gibt auch andere Typen, Variablen und „normale“ Funktionen, die nicht zu
einer Klasse gehören.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6–4
6 Klassen und objektorientierte Konzepte
Java
Eine Klasse ist ein sprach- oder programmiererdefinierter Typ. Es gibt dafür ein spezielles Konstrukt. Es gibt auch andere Typen. Es gibt keine Variablen und Funktionen,
die nicht zu einer Klasse gehören.
Tabelle 6.2 Eigenschaften von Klassen
Eigenschaft
Klasse realisierbar
mit Sprachkonstrukt
spezielles Klassenkonstrukt
Verhältnis zu
Typsystem
nichtgenerische
Klasse ist
generische Klasse
Component
Pascal
Eiffel
C++
Java
C#
RECORD
class
struct
union
class
class
struct
class
nein
programmiererdefinierter Typ
-
ja
sprach- oder
programmiererdefinierter Typ
programmiererdefinierter Typ
beschreibt Menge programmiererdefinierter Typen
Nicht-Klassentypen
klassenexterne Daten
ja
ja
nein
klassenexterne Operationen
6.2.1
sprach- oder
programmiererdefinierter Typ
ja
nein
nein
Bestandteile von Klassen
Attribute
Methoden
Konstruktoren
Destruktoren
6.2.2
Verhältnis zu Kompilationseinheiten
Bei allen fünf Sprachen ist der Inhalt einer Datei Kompilationseinheit.
Component Pascal
Eine Klasse ist vollständig in einem Modul enthalten. Ein Modul kann mehrere Klassen enthalten. Kompilationseinheit ist das Modul.
Eiffel
Klassen sind die einzigen Kompilationseinheiten.
C++
Die Definition einer Klasse steht üblicherweise in einer Schnittstellendatei, die Definitionen der Elementfunktionen der Klasse in der zugehörigen Implementationsdatei,
außer bei generischen Klassen.
Eine Datei kann mehrere Klassendefinitionen enthalten. Die Teile einer Klasse können
auf mehrere Dateien verteilt werden. Beispielsweise könnte jede Funktionsdefinition in
einer anderen Datei stehen; die Klassendefinition könnte mit Inkludierung aus verstreuten Teilen zusammengesetzt sein.
Vermutlich ist es sinnvoll, von den vielfältigen Verteilungsmöglichkeiten nur wenige
zu nutzen, z.B. die erstgenannte.
Java, C#
In einer Datei können mehrere Klassen aufeinander folgen und eine Kompilationseinheit bilden.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.3 Objekte
6.3
6–5
Objekte
Allgemein können Variablen in folgenden drei Speicherbereichen liegen:
außerhalb von Routinen und Klassen statisch vereinbarte Variablen im globalen
Datenbereich;
in Routinen statisch vereinbarte Variablen (und Parameter) im Laufzeitkeller (runtime stack);
dynamisch erzeugte Variablen auf der Halde (heap).
Bei statisch vereinbarten Variablen im globalen Datenbereich und im Laufzeitkeller
sprechen wir von Wertsemantik, bei dynamisch erzeugten Variablen auf der Halde
von Referenzsemantik.
Die Programmiersprachen unterscheiden sich darin, in welchen Speicherbereichen
Objekte liegen können. Alle Sprachen können Objekte einer Klasse dynamisch erzeugen. Component Pascal und C++ können von derselben Klasse Objekte statisch vereinbaren oder dynamisch erzeugen. Eiffel und C# kennen Klassen mit Referenzsemantik und Klassen mit Wertsemantik. Eiffel kann von einer Klasse mit Referenzsemantik
ein Wertobjekt vereinbaren, aber nur, wenn die Klasse genau eine parameterlose Erzeugungsprozedur hat. In C# sind Klassen mit Wertsemantik nicht beerbbar oder erbend,
d.h. es sind nur ADTen.
Tabelle 6.3 Wert- und Referenzsemantik
Eigenschaft
Component Pascal
statisches Wertobjekt
(globaler Datenbereich,
Keller)
Eiffel
C++
ja
dynamisches Referenzobjekt
(Halde)
Name für aktuelles Objekt
(von dem ein Dienst
aufgerufen ist)
Java
C#
nein
ja
ja
frei wählbar
Referenzparameter von Objekt oder
Wertparameter von Zeiger auf Objekt
Current
this
this
Referenz
Zeiger
Referenz
Wir zeigen anhand der Datumsklasse aus 2.3 S. 2-26 beispielhaft, wie man zu einem
Wertobjekt und einem Referenzobjekt kommt und damit umgeht.
6.3.1
Component Pascal
Beispiel für Wertobjekt
VAR date : DateDesc;
...
date.Init;
date.Set (1997, 3, 7);
date.Advance;
(* RECORD *)
Eiffel
date : expanded DATE
...
date.set (1997, 3, 7)
date.advance
-- Standardinitialisierung mit make
C++
Date date;
...
date.set(1997, 3, 7);
date.advance;
// Standardinitialisierung mit Date ()
Java
27.9.12
(* Explizite Initialisierung *)
In Java gibt es keine Wertobjekte.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6–6
6 Klassen und objektorientierte Konzepte
C#
In C# gibt es zwar Wertobjekte, aber nur als Exemplare von Typen, die mit dem
struct-Konstrukt definiert sind. In der Definition wäre also class durch struct zu
ersetzen. struct-Klassen sind nicht beerbbar. Der Programmierer muss schon bei der
Definition der Klasse entscheiden, ob es davon nur Wertobjekte oder nur Referenzobjekte geben soll. Dies widerspricht dem Hellseherprinzip.
6.3.2
Beispiel für Referenzobjekt
Component Pascal
VAR date : Date;
...
NEW (date);
date^.Init;
date.Set (1997, 3, 7);
date.Advance;
Eiffel
(* POINTER TO RECORD *)
(* Explizite Dereferenzierung *)
(* Implizite Dereferenzierung *)
date : DATE
...
create date.make
date.set (1997, 3, 7)
date.advance
C++
-- Explizite Erzeugung und Initialisierung
-- Implizite Dereferenzierung
Mit Referenz:
Date & date = Date();
...
date.set(1997, 3, 7);
date.advance();
// Explizite Erzeugung und Initialisierung
// Implizite Dereferenzierung
Mit Zeiger:
Date * date = new Date();
...
(*date).set(1997, 3, 7);
date->advance();
Java, C#
Date date = new Date();
...
date.set(1997, 3, 7);
date.advance();
6.3.3
// Explizite Erzeugung und Initialisierung
// Explizite Dereferenzierung
// Explizite Dereferenzierung
// Explizite Erzeugung und Initialisierung
// Implizite Dereferenzierung
Zugriffe auf Objektmerkmale
obj
bezeichnet jeweils ein Objekt, fea ein Merkmal.
Tabelle 6.4 Zugriffe auf Objektmerkmale
Zugriff auf
Component
Pascal
Eiffel
C++
Attribut eines Wertobjekts
obj.fea
obj.fea
obj.fea
parameterlose Funktion
eines Wertobjekts
obj.fea ()
obj.fea
obj.fea()
Attribut
eines Referenzobjekts
obj^.fea
obj.fea
obj.fea
obj^.fea ()
obj.fea ()
obj.fea
parameterlose Funktion
eines Referenzobjekts
Java
C#
obj.fea
obj.fea()
Zeiger: (*obj).fea
obj->fea
obj.fea
obj.fea
obj.fea()
obj.fea()
Referenz: obj.fea
Zeiger: (*obj).fea()
obj->fea()
Referenz: obj.fea()
Dem Konzept der Bezugstransparenz, d.h. der Unabhängigkeit des Zugriffs von der
Implementation, entspricht offenbar Eiffel am besten. Die Implementationen des
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.4 Klassendaten und -operationen
6–7
Objekts (statisch/dynamisch) und des Merkmals (Attribut/Funktion) können ohne Auswirkung auf die Benutzung geändert werden.
6.4
Klassendaten und -operationen
6.4.1
Klassendaten
Die Definition einer Klasse beschreibt, welche Attribute jedes Objekt dieser Klasse hat
und welche Operationen mit diesem Objekt ausführbar sind. Attribute beschreiben also
normalerweise Objektdaten, d.h. es wird exemplarweise Speicherplatz für sie belegt.
Manchmal braucht man Attribute, die für alle Objekte nur einmal vorhanden sind, also
der Klasse zugeordnet sind, diese nennen wir Klassendaten. Es wird nur einmal Speicherplatz für sie belegt, alle Objekte arbeiten mit dem gemeinsamen Speicherplatz, d.h.
Wert des Attributs.
Component Pascal
Es gibt kein spezielles Konstrukt für Klassendaten. Man realisiert sie am besten als
nichtexportierte Variable in dem Modul, das die Klassendefinition enthält.
MODULE M;
TYPE AClass* = RECORD ... END;
VAR classData : SomeType;
PROCEDURE (VAR a : AClass) Proc* (...);
BEGIN
Zugriff auf classData und auf Objektdaten;
END Proc;
BEGIN
Initialisierung von classData;
END M.
Eiffel
Es gibt kein spezielles Konstrukt für Klassendaten. Man realisiert sie am besten mit
einer geschützten Einmalfunktion.
Die Klassendaten werden einmal dynamisch erzeugt, jedes Objekt erhält bei jedem
Zugriff eine Referenz auf dasselbe Exemplar der Klassendaten.
class A_CLASS
feature {NONE}
class_data : SOME_TYPE
once
create Result.make (...)
end -- class_data
feature
proc (...)
do
Zugriff auf class_data und auf Objektdaten
end -- proc
end -- class A_CLASS
C++
Klassendaten sind mit dem Speicherklassenspezifikator static realisierbar.
class A_class {
protected:
static Some_type class_data;
public:
void proc (...);
};
Some_type A_class::class_data = Initialisierung;
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6–8
6 Klassen und objektorientierte Konzepte
void A_class::proc (...) {
Zugriff auf class_data und auf Objektdaten;
}
Java
Klassendaten sind mit dem Speicherklassenmodifikator static realisierbar.
class AClass {
private static SomeType classData = Initialisierung;
public void proc (...) {
Zugriff auf classData und auf Objektdaten;
}
}
C#
Klassendaten sind mit dem Speicherklassenmodifikator static realisierbar.
class AClass {
protected static SomeType classData = Initialisierung;
public void Proc (...) {
Zugriff auf classData und auf Objektdaten;
}
}
6.4.2
Klassenoperationen
Unterstützt eine Sprache Klassendaten, so kann eine Klasse Operationen haben, die nur
auf Klassendaten, nicht auf Objektdaten zugreifen. Wir nennen solche Operatioen
Klassenoperationen. Für sie macht es Sinn, sie an die Klasse, nicht ein Objekt zu binden, d.h. sie mit dem Klassennamen, nicht einem Objektnamen qualifiziert aufzurufen.
Component Pascal
Es gibt kein spezielles Konstrukt für Klassenoperationen. Man realisiert sie am besten
als Prozeduren in dem Modul, das die Klassendefinition enthält.
MODULE M;
TYPE AClass* = RECORD ... END;
VAR classData : SomeType;
PROCEDURE ClassProc* (...);
BEGIN
Zugriff nur auf classData;
END ClassProc;
BEGIN
Initialisierung von classData;
END M.
Benutzung:
M.ClassProc (...);
Eiffel
Es gibt kein Konstrukt für Klassenoperationen. Man realisiert sie als normale Objektoperationen. Zu ihrer Benutzung muss stets ein Objekt erzeugt werden.
C++
Der Spezifikator static ist auch auf Elementfunktionen anwendbar. Die Funktion kann
dann mit dem Klassennamen qualifiziert aufgerufen werden, ohne dass ein Objekt dieser Klasse existieren muss.
class A_class {
protected:
static Some_type class_data;
public:
static void class_proc (...);
};
Some_type A_class::class_data = Initialisierung;
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.5 Abstrakte Klassen
6–9
void A_class::class_proc (...) {
Zugriff nur auf class_data;
}
Benutzung:
A_class::class_proc (...);
Java, C#
Klassenfunktionen sind mit dem Speicherklassenmodifikator static realisierbar.
class AClass {
protected static SomeType classData = Initialisierung;
public static void classProc (...) {
Zugriff nur auf classData;
}
}
Benutzung:
AClass.classProc (...);
6.5
Abstrakte Klassen
6.5.1
Gemeinsames
Alle fünf Sprachen unterstützen abstrakte Dienste und Klassen. Ein Dienst ist abstrakt
(abstract), wenn seine Signatur ohne eine zugeordnete Implementation deklariert ist.
Eine Klasse ist abstrakt, wenn sie wenigstens einen abstrakten Dienst enthält. Von
einer abstrakten Klasse können keine Objekte erzeugt werden. Abstrakte Klassen dienen dazu, Schnittstellen und partielle Implementationen für Unterklassen festzulegen.
Sie sind ein wichiger Aspekt des zentralen Abstraktionskonzepts der Vererbung.
6.5.2
Unterschiedliches
Component Pascal
Das reservierte Wort ABSTRACT markiert abstrakte Prozeduren und Klassen. Eine
Klasse ist abstrakt, wenn sie wenigstens eine abstrakte Prozedur enthält.
TYPE AnAbstractClass* = ABSTRACT RECORD ... END;
PROCEDURE (VAR a : AnAbstractClass) DoSomething* (...), ABSTRACT;
Ein Übersetzer braucht die zusätzliche Markierung des Typs mit ABSTRACT nicht;
diese verbessert die Lesbarkeit.
Eiffel
Das reservierte Wort deferred markiert abstrakte (aufgeschobene) Routinen und Klassen. Eine Klasse ist aufgeschoben, wenn sie wenigstens eine aufgeschobene Routine
enthält.
deferred class AN_ABSTRACT_CLASS
feature
do_something (...)
deferred
end
end -- class AN_ABSTRACT_CLASS
C++
Der Pur-Spezifikator „= 0“ markiert abstrakte Funktionen. Eine Klasse ist abstrakt,
wenn sie wenigstens eine pure virtuelle Funktion enthält.
class An_abstract_class {
public:
virtual void do_something (...) = 0;
};
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 – 10
6 Klassen und objektorientierte Konzepte
Stroustrup wählte die irritierende pseudoalgebraische Notation „= 0“ zur Markierung
fehlender Implementationen, um kein weiteres Schlüsselwort (etwa abstract) einführen
zu müssen [Str94a]. Der Kopf einer abstrakten Klasse wird nicht zusätzlich markiert.
Java, C#
Das reservierte Wort abstract markiert abstrakte Methoden und Klassen. Eine Klasse
ist abstrakt, wenn sie wenigstens eine abstrakte Methode enthält.
public abstract class AnAbstractClass {
public abstract void doSomething (...);
}
6.6
Vererbung
Tabelle 6.5 Vererbung
Eigenschaft
Component Pascal
Eiffel
C++
Java
C#
Art der Vererbung
(Anzahl Oberklassen)
einfach
mehrfach,
wiederholt
mehrfach
modulextern:
durch Exportmarken
des Basistyps gesteuert
vollständig
durch Zugriffsmodifikator private
auf Basisklasse einschränkbar
ABSTRACT
deferred
einfach,
Schnittstellen mehrfach
modulintern: vollständig
Zugriff auf
geerbte Merkmale
Konstrukt für
abstrakten Dienst/abstrakte Klasse
Konstrukt für
Schnittstelle
nein
ANYREC
ANYPTR
ANY
Unterklasse
aller Klassen
nein
NONE
Baum
vollständiger
Verband
Component Pascal
abstract
interface I {...}
Oberklasse
aller Klassen
Vererbungsstruktur
=0
nein
Object
nein
Halbordnung
halbvollständiger
Verband
TYPE A =
RECORD
Felder für A
END;
Prozeduren für A
TYPE B =
RECORD (A)
zusätzliche Felder für B
END;
zusätzliche und redefinierte Prozeduren für B
Eiffel
class A
Merkmale für A
end
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.7 Anpassung geerbter Merkmale
6 – 11
class B
inherit A
redefine
Redefinitionsdeklarationen
end
feature
zusätzliche und redeklarierte Merkmale für B
end
C++
class A {
Elemente für A
};
Elementfunktionsdefinitionen für A
class B : public A {
zusätzliche Elemente und Definitionen redefinierter Elementfunktionsdefinitionen für B
};
zusätzliche und redefinierte Elementfunktionsdefinitionen für B
Java
public class A {
Elemente für A
}
public class B extends A {
zusätzliche und überschriebene Elemente für B
}
C#
public class A {
Elemente für A
}
public class B : A {
zusätzliche und überschriebene Elemente für B
}
6.7
Anpassung geerbter Merkmale
Welche Möglichkeiten hat eine Unterklasse, ein von einer Oberklasse geerbtes Merkmal für ihre Zwecke anzupassen?
Die wichtigste Möglichkeit ist die Redefinition geerbter Operationen. Diese ist in allen
fünf Sprachen vorhanden. Die Redefinitionsregeln sind in 6.9 genannt.
Component Pascal
Typgebundene Prozeduren können in Erweiterungstypen redefiniert werden, wenn sie
im Basistyp als abstrakt (ABSTRACT) oder erweiterbar (EXTENSIBLE) markiert sind.
Möglichkeiten:
Definieren einer abstrakten typgebundenen Prozedur
Redefinieren einer erweiterbaren typgebundenen Prozedur
Typgebundene Prozeduren können als nur redefinierbar exportiert werden (Exportmarke „-“). Der erweiternde Typ darf solche Prozeduren redefinieren, aber nicht
selbst aufrufen. Sinnvoll ist das für die Implementation von Schablonenmethoden,
die oft in Frameworks vorkommen. Dies ist ein Beispiel dafür, wie Sprachkonstrukte Entwurfsmuster unterstützen können.
kovariantes Ändern des Ergebnistyps einer typgebundenen Funktion
Einschränken des Exportstatus eines Merkmals (zum Erzwingen polymorpher
Benutzung), sofern beide Typen im selben Modul vereinbart sind
Entziehen der Redefinierbarkeit einer typgebundenen Prozedur (durch fehlendes
EXTENSIBLE)
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 – 12
Eiffel
6 Klassen und objektorientierte Konzepte
Jede geerbte Routine kann redefiniert werden, es sei denn, sie ist in der Vorgängerklasse als gefroren spezifiziert (frozen).
Möglichkeiten:
Definieren (Effektivieren) einer abstrakten Routine
Redefinieren einer konkreten Routine
Ändern des Vertrags einer Routine: Vorbedingung abschwächen, Nachbedingung
verstärken
kovariantes Ändern der Signatur einer Routine
Redefinieren einer Funktion als Attribut
Aufheben der Definition einer Routine
Ändern des Exportstatus eines Merkmals
Umbenennen eines Merkmals
Verschmelzen mehrerer Merkmale gleicher Signatur zu einem Merkmal
Replizieren eines Merkmals
Entziehen der Redefinierbarkeit eines Merkmals (mit frozen)
Es zeigt sich, dass Eiffel hier als rein objektorientierte Sprache viele Möglichkeiten bietet. Diese sind aber für den Anfang und viele Anwendungen nicht unbedingt erforderlich; bei ungeschickter Programmierung können sie auch Probleme verursachen.
C++
Eine geerbte Elementfunktion kann redefiniert (überschrieben) werden, wenn sie in der
Basisklasse, in der sie zuerst definiert ist, als virtuell spezifiziert ist (virtual).
Möglichkeiten:
Java
Definieren, d.h. Konkretisieren einer abstrakten (puren virtuellen) Elementfunktion
Überschreiben einer konkreten virtuellen Elementfunktion
Redefinieren einer konkreten virtuellen Elementfunktion als abstrakt
kovariantes Ändern des Ergebnistyps einer virtuellen Elementfunktion
Einschränken des Exportstatus eines Elements (mit protected, private)
Eine geerbte Methode kann redefiniert (überschrieben) werden, es sei denn, sie ist in
der Oberklasse als final spezifiziert (final).
Möglichkeiten:
C#
Definieren, d.h. Konkretisieren einer abstrakten Methode
Überschreiben einer konkreten Methode
kovariantes Ändern des Ergebnistyps einer Methode
Einschränken des Exportstatus einer Methode (mit protected, private)
Entziehen der Redefinierbarkeit einer Methode (mit final)
C# zu ergänzen.
6.8
Polymorphie und dynamisches Binden
6.8.1
Gemeinsames
Polymorphie
Alle fünf Sprachen bieten das zentrale objektorientierte Konzept der Polymorphie und
des dynamischen Bindens: Bei polymorphen Größen können aufgerufene Dienste
abhängig von ihrem dynamischen Typ dynamisch gebunden werden. Eine Größe ist
eine parameterlose Abfrage (Attribut oder Funktion), ein Formalparameter oder eine
Variable. Polymorphie bedeutet allgemein Vielgestaltigkeit, hier die Eigenschaft einer
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.8 Polymorphie und dynamisches Binden
6 – 13
Größe, verschiedene Gestalten anzunehmen. Eine Größe ist polymorph, wenn sie sich
auf Objekte verschiedener Typen beziehen kann.
Statischer und
dynamischer Typ
Alle fünf Sprachen sind statisch typisiert, d.h. sie binden jede Größe bei ihrer Vereinbarung an einen Typ – ihren statischen Typ, der zur Übersetzungszeit festgelegt ist. Polymorphe Größen sind zudem zur Laufzeit an einen dynamischen Typ gebunden. Der
dynamische Typ einer Größe steht in einer Vererbungsbeziehung zu ihrem statischen
Typ: Der dynamische Typ kann ein Untertyp ihres statischen Typs sein.
Polymorph oder nicht?
Welche Größen können polymorph sein? Eine Größe mit Wertsemantik bezieht sich
stets auf ein festes Wertobjekt ihres statischen Typs; bei ihr sind also statischer und
dynamischer Typ identisch. Daher sind Wertgrößen nicht polymorph. Nur Referenzund Zeigergrößen können polymorph sein, nur bei diesen können sich statischer und
dynamischer Typ unterscheiden, nur diese lassen sich zur Laufzeit an Referenzobjekte
verschiedener Untertypen ihres statischen Typs binden.
Semantik zählt
Im Folgenden bedeutet Aufruf mit Semantik dynamischen Bindens nur, dass der
Effekt des Aufrufs wie unter dynamischem Binden ist. Daraus folgt nicht, dass unbedingt dynamisch gebunden wird. Wo es ohne Änderung der Semantik möglich ist, kann
ein Kompilierer einen Aufruf auch statisch binden, z.B. wenn die Zielgröße des Aufrufs nicht polymorph oder der aufgerufene Dienst nicht redefinierbar ist.
Das Konzept der Polymorphie und des dynamisches Bindens ermöglicht flexibel
anpassbare und erweiterbare Komponenten. Abgeschlossene, vollständig implementierte Lieferanten können Kunden mit spezifischen Aufträgen bedienen, indem die
Kunden von Lieferanten spezifizierte Dienste in eigenen Unterklassen redefinieren und
den Lieferanten eigene Objekte zur Bearbeitung übergeben. Viele objektorientierte
Entwurfsmuster beruhen auf Polymorphie und dynamischem Binden [GHJV04].
6.8.2
Historisches
Schon Simual 67, die erste objektorientierte Sprache, führt das Konzept der Polymorphie und des dynamisches Bindens ein, allerdings nicht als Standard, sondern als
Option neben statischem Binden. Smalltalk erhebt Polymorphie und dynamisches Binden zum Standard, allerdings in rein dynamisch typisiertem Kontext. Oberon-2, Component Pascal und Eiffel verbinden die Standardsemantik dynamischen Bindens mit
statischer Typisierung; Java folgt dieser Entwicklungslinie. C++ und Borlands Object
Pascal übernehmen das Doppelmodell von Simula 67; C# folgt ihnen dabei.
6.8.3
Component Pascal
Beim Aufruf einer typgebundenen Prozedur gilt genau dann die Semantik dynamischen
Bindens, wenn auf das Objekt mit einem Referenzparameter oder einer Zeigervariablen
zugegriffen wird.
6.8.4
Eiffel
Beim Aufruf von Merkmalen gilt generell die Semantik dynamischen Bindens.
Vereinbarung
deferred class A
-- deferred means abstract.
feature
f deferred end
-- implicitly redefinable, to be dynamically bound
g do print ("A.g") end
-- implicitly redefinable, to be dynamically bound
end -- class A
class B inherit A
redefine
g
end
27.9.12
-- f may be listed, but must not.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 – 14
6 Klassen und objektorientierte Konzepte
feature
f do print ("B.f") end
g do print ("B.g") end
-- redeclares, effects A.f
-- redeclares, redefines A.g
end -- class B
class C inherit B
redefine
f,
g
end
feature
f do print ("C.f") end
g do print ("C.g") end
-- redeclares, redefines B.f
-- redeclares, redefines B.g
end -- class C
Benutzung
a:A
...
-- polymorphic entity a of static type A
create {B} a
a.f
a.g
-- dynamic type of a is B
-- dynamically bound to B.f
-- dynamically bound to B.g
create {C} a
a.f
a.g
-- dynamic type of a is C
-- dynamically bound to C.f
-- dynamically bound to C.g
Ein in großen Softwareprojekten praktisch auftretendes Problem ist versehentliches
Redefinieren einer konkreten Routine. Als Folge kann sich durch polymorphe Aufrufe
das Systemverhalten ändern, ohne dass die Ursache leicht erkennbar ist. Das Problem
versehentlichen Redefinierens löst der redefine-Abschnitt: Redefinieren ohne explizite
redefine-Deklaration ist ein Kontextfehler. Ist die Vorgängerversion der Routine abstrakt, so kann eine redefine-Deklaration hingeschrieben werden, muss aber nicht.
Warum nicht? Eine abstrakte Routine ist nicht ausführbar; versehentliches Effektivieren einer abstrakten Routine kann kein anderes Systemverhalten bewirken.
6.8.5
C++
Beim Aufruf einer Elementfunktion gilt die Semantik dynamischen Bindens, wenn gilt:
(1)
(2)
(3)
(4)
(5)
Auf das Objekt wird mit einer Referenz oder einem Zeiger q des statischen Typs
A zugegriffen, dabei wird eine Funktion namens f aufgerufen.
In A oder einer Basisklasse von A ist eine als virtual spezifizierte Funktion f (die
damit weder eine Klassenfunktion noch eine Inline-Funktion ist) mit der Parameterliste P definiert, E sei ihr Ergebnistyp.
Der dynamische Typ von q sei B. B ist eine von A abgeleitete Klasse, und in B
oder einer von A abgeleiteten Basisklasse von B ist eine Funktion f mit der Parameterliste P und dem Ergebnistyp F definiert.
F ist gleich E oder eine von E abgeleitete Klasse.
Die aktuellen Parameter des Aufrufs von f über q sind mit den formalen Parametern der in A gültigen Definition von f kompatibel.
Unter diesen Voraussetzungen wird die in B gültige Version von f aufgerufen.
Vereinbarung
class A {
public:
virtual void f () = 0;
virtual void g ();
-- abstract, overridable, to be dynamically bound
-- overridable, to be dynamically bound
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.8 Polymorphie und dynamisches Binden
6 – 15
void h ();
-- not overridable, to be statically bound
}
void A::g () { cout << "A::g"; }
void A::h () { cout << "A::h"; }
class B : public A {
public:
void f ();
-- overrides A::f, still virtual, to be dynamically bound
void g ();
-- overrides A::g, still virtual, to be dynamically bound
void h ();
-- hides A::h, to be statically bound
}
void B::f () { cout << "B::f"; }
void B::g () { cout << "B::g"; }
void B::h () { cout << "B::h"; }
class C : public B {
public:
virtual void f ();
-- overrides B::f, still virtual, to be dynamically bound
void g ();
-- overrides B::g, still virtual, to be dynamically bound
virtual void h ();
-- hides B::h, to be dynamically bound via C-pointers
}
void C::f () { cout << "C::f"; }
void C::g () { cout << "C::g"; }
void C::h () { cout << "C::h"; }
Benutzung
A * a;
...
-- polymorphic pointer a of static type A*
a = new B();
a->f();
a->g();
a->h();
-- dynamic type of a is B*
-- dynamically bound to B::f
-- dynamically bound to B::g
-- statically bound to A::h
a = new C();
a->f();
a->g();
a->h();
-- dynamic type of a is C*
-- dynamically bound to C::f
-- dynamically bound to C::g
-- statically bound to A::h
Aus dem Quelltext der Klassen B und C ist nicht ersichtlich, welche Funktionen
überschreiben (f, g) und welche verdecken (h), und bei welchen wann dynamisch
oder statisch gebunden wird.
Gemischtes Vorkommen von virtuellen, dynamisch gebundenen und nichtvirtuellen,
statisch gebundenen Funktionen kann verwirren.
Welches Problem wäre mit nichtvirtuellen, statisch gebundenen Funktionen zu
lösen? Dem Autor ist keins bekannt.
Kein objektorientiertes Entwurfsmuster beruht auf statischem Binden [GHJV04].
Eine in einer abgeleiteten Klasse definierte Funktion kann versehentlich eine
geerbte virtuelle Funktion überschreiben, da weder Neudefinieren noch Überschreiben markiert werden muss.
Auch das gemischte Vorkommen von virtuellen und überladenen Funktionen kann
verwirren, da überladene Funktionen statisch gebunden werden.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 – 16
Leitlinie 6.1
C++: Vermeide
Verdecken in
abgeleiteten Klassen
6 Klassen und objektorientierte Konzepte
Die Vereinbarung eines Elements in einer abgeleiteten Klasse verdeckt die Vereinbarung eines gleichnamigen Elements in einer Basisklasse. Vermeide diese spezielle
Art des Verdeckens, da sie verwirren und versehentlich zu statischem Binden führen
kann, wo dynamisches Binden erwartet wird!
Leitlinie 6.2
C++: Vermeide
Überladen virtueller
Funktionen
Vermeide wenigstens das Überladen virtueller Funktionen mit nichtvirtuellen Funktionen!
Leitlinie 6.3
C++: Vereinbare
Elementfunktionen
virtuell
Vereinbare neue Elementfunktionen wo möglich virtuell, damit sie überschreibbar
sind und die Semantik dynamischen Bindens gilt! Ausgenommen sind statische und
Inline-Funktionen.
6.8.6
Java
Beim Aufruf von Methoden – außer bei (mit static vereinbarten) Klassenmethoden –
gilt die Semantik dynamischen Bindens.
Vereinbarung
public abstract class A {
public abstract void f ();
-- abstract, overridable, to be dynamically bound
public void g () { System.out.print ("A.g"); };
-- overridable, to be dynamically bound
}
public class B extends A {
@Override
public void f () { System.out.print ("B.f"); };
-- overrides A.f, to be dynamically bound
@Override
public void g () { System.out.print ("B.g"); };
-- overrides A.g, to be dynamically bound
}
public class C extends B {
@Override
public void f () { System.out.print ("C.f"); };
-- overrides B.f, to be dynamically bound
@Override
public void g () { System.out.print ("C.g"); };
-- overrides B.g, to be dynamically bound
}
Benutzung
A a;
...
-- polymorphic reference a of static type A
a = new B();
a.f();
a.g();
-- dynamic type of a is B
-- dynamically bound to B.f
-- dynamically bound to B.g
a = new C();
a.f();
a.g();
-- dynamic type of a is C
-- dynamically bound to C.f
-- dynamically bound to C.g
Obwohl in Java wie in Component Pascal und Eiffel die Semantik dynamischen Bindens Standard ist, können in diesem Zusammenhang statisches Binden von Klassenmethoden und Verdecken, Überladen, Überschatten und Verdunkeln von Namen zu Verwirrungen führen. Zwar informiert die optionale @Override-Annotation ähnlich wie
Eiffels redefine-Abschnitt den Kompilierer, dass die Methode eine geerbte Methode
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.8 Polymorphie und dynamisches Binden
6 – 17
überschreiben soll, sodass er einen Fehler melden kann, falls keine Oberklasse eine
passende Methode hat. Während jedoch Eiffel das Verdecken von Namen verbietet und
damit auch den Fehler erkennt, dass in einer Unterklasse ein vermeintlich neues Merkmal definiert wird, wo schon ein gleichnamiges in einer Oberklasse existiert, kann die
@Override-Annotation diesen Fehler nicht erkennen. So bleibt versehentliches Verdecken und Neudefinieren von Namen eine Fehlerquelle in Java.
Leitlinie 6.4
Java: Benutze
@Override
Leitlinie 6.5
Java: Vermeide
Verwirrendes
6.8.7
Markiere überschreibende Methoden mit der @Override-Annotation, da dies die
Lesbarkeit verbessert und Fehler vermeiden hilft!
Vermeide Verdecken, Überschatten und Verdunkeln von Namen, da sie verwirren!
Vermeide möglichst auch Überladen, da es im Zusammenhang mit Überschreiben
verwirren kann! Studiere die Beispiele in [BlN05]!
C#
C# bietet ähnlich wie Simula 67 und C++ sowohl statisches als auch dynamisches Binden.
Vereinbarung
public abstract class A {
public abstract void f ();
-- abstract, overridable, to be dynamically bound
public virtual void g () { System.Console.Write ("A.g"); };
-- overridable, to be dynamically bound
public void h () { System.Console.Write ("A.h"); };
-- not overridable, to be statically bound
}
public class B : A {
public override void f () { System.Console.Write ("B.f"); };
-- overrides A.f, to be dynamically bound
public override void g () { System.Console.Write ("B.g"); };
-- overrides A.g, to be dynamically bound
public new void h () { System.Console.Write ("B.h"); };
-- hides A.h, to be statically bound
}
public class C extends B {
public override void f () { System.Console.Write ("C.f"); };
-- overrides B.f, to be dynamically bound
public void g () { System.Console.Write ("C.g"); };
-- hides B.g, warning issued, statically bound
public virtual void h () { System.Console.Write ("C.g"); };
-- hides B.g, warning issued, dynamically bound via C-variables
}
Benutzung
27.9.12
A a;
...
-- polymorphic reference a of static type A
a = new B();
a.f();
a.g();
a.h();
-----
dynamic type of a is B
dynamically bound to B.f
dynamically bound to B.g
statically bound to A.h
a = new C();
a.f();
a.g();
a.h();
-----
dynamic type of a is C
dynamically bound to C.f
dynamically bound to C.g
statically bound to A.h
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 – 18
6 Klassen und objektorientierte Konzepte
C# folgt hier eher C++ als Java, stopft aber die Fehlerquellen versehentliches Überschreiben, Neudefinieren und Verdecken. Dennoch bleiben folgende Probleme offen:
Gemischtes Vorkommen von virtuellen, dynamisch gebundenen und nichtvirtuellen,
statisch gebundenen Methoden kann verwirren.
Welches Problem wäre mit nichtvirtuellen, statisch gebundenen Methoden zu lösen?
Kein objektorientiertes Entwurfsmuster beruht auf statischem Binden [GHJV04].
6.9
Redefinitionsregeln
Sei A eine Klasse, in der die Operation op definiert ist. Sei B eine Unterklasse von A, in
der op mit demselben Namen redefiniert ist.
Component Pascal,
C++
Es gilt die Invarianzregel für die Parameter und die Kovarianzregel für das Ergebnis
von op. Das heißt: op in A und op in B haben dieselbe Anzahl von Parametern, und die
Typen der Parameter sind gleich. Der Ergebnistyp von op in B ist eine Erweiterung
bzw. Ableitung des Ergebnistyps von op in A.
Eiffel
Es gilt die Kovarianzregel für die Parameter und das Ergebnis von op. Das heißt: op in
A und op in B haben dieselbe Anzahl von Parametern, und die Typen der Parameter und
des Ergebnisses von op in B sind jeweils Unterklassen der entsprechenden Typen von
op in A.
C++
In früheren Versionen von C++ galt auch für das Funktionsergebnis die Invarianzregel.
Hier soll auch auf einige Besonderheiten von C++ hingewiesen werden.
In A und B können Funktionen mit demselben Namen, aber unterschiedlichen Parameterlisten definiert sein. In diesem Fall handelt es sich nicht um Redefinition, sondern
um Überladen (overloading).
Die Mischung der Mechanismen Redefinition und Überladen – insbesondere verquickt
mit dem Mechanismus der Standardparameter – kann zu Verwirrung beitragen.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
6.10
Zugriffe auf Oberklassenversionen geerbter Operationen
Das folgende Problem stellt sich oft: Sei A eine Klasse, in der die Operation op definiert ist. Sei B eine direkte Unterklasse von A, in der op mit demselben Namen redefiniert ist. Wie kann die B-Version von op die A-Version von op aufrufen?
Component Pascal
Die in der Erweiterungslinie unmittelbare Vorgängerversion von op kann durch Anhängen von ^ an den Namen relativ bezeichnet werden:
op^ (...);
Dieser Mechanismus stammt von Smalltalk. Da in einer Operation Vorgängerversionen
beliebiger anderer Operationen aufrufbar sind, ist der Mechanismus mächtiger als das
Problem verlangt, er gefährdet dadurch aber das Geheimnisprinzip.
Eiffel
In frühen Eiffel-Versionen mussten die Mechanismen wiederholtes Erben und Umbenennen geerbter Merkmale eingesetzt werden, um die unmittelbare Vorgängerversion
von op bezeichnen zu können.
class B
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.11 Schachtelung
6 – 19
inherit
A
rename
op as op_A
export
{NONE} all
end
A
redefine
op
select
op
end
feature
op (...)
do
...
op_A (...)
...
end -- op
end -- class B
Dieses umständliche, durch rename-, export-, redefine- und select-Abschnitte schwergewichtige Muster wurde unnötig mit dem 1997 in den NICE-Standard eingeführten Precursor-Konstrukt. Die unmittelbare Vorgängerversion von op hat einfach den relativen
Standardnamen Precursor:
op (...)
do
...
Precursor (...)
...
end -- op
C++
Alle Vorgängerversionen von op können absolut durch Qualifizierung mit dem Klassennamen bezeichnet werden.
A::op(...);
Dieser Mechanismus ist einerseits mächtiger als der von Component Pascal. Andererseits zeigen sich seine Grenzen bei indirekter wiederholter Vererbung und bei Erweiterungen von Vererbungsstrukturen.
Java
Java bietet mit dem super-Konstrukt denselben Mechanismus wie Smalltalk und Component Pascal. Die unmittelbare Vorgängerversion von op kann durch Qualifizieren
mit super relativ bezeichnet werden:
super.op(...);
C#
C# zu ergänzen.
6.11
Schachtelung
Hier geht es um die Frage, welche Strukturierungseinheiten textuell ineinandergeschachtelt werden können. Damit ist auch eine Steuerung der Sichtbarkeitsbereiche
verbunden.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 – 20
6 Klassen und objektorientierte Konzepte
Tabelle 6.6 Schachtelung
Eigenschaft
Component
Pascal
Prozedur in Prozedur
ja
nein
nein
-
Modul in Modul
Klasse in Klasse
C++
Java
nein
Prozedur in Modul
ja
Operation in Klasse
logisch ja,
textuell nein
Klasse in Modul
Eiffel
C#
ja
ja
logisch u. Inline-Funktion
ja, textuell bzw. sonst nein
ja
ja
-
6.12
Speicherverwaltung
Component Pascal,
Eiffel, Java, C#
Bei diesen Sprachen übernimmt das Laufzeitsystem die automatische Speicherbereinigung (garbage collection) für nicht mehr benutzte dynamisch erzeugte Objekte.
C++
Es gibt keine automatische Speicherbereinigung. Die EntwicklerIn muss die Speicherverwaltung selbst programmieren, indem sie zu jeder Klasse geeignete Löschoperationen definiert.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
6.12 Speicherverwaltung
27.9.12
6 – 21
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 – 22
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
6 Klassen und objektorientierte Konzepte
27.9.12
7
Typen und Datenkonzepte
Aufgabe 7
7.1
Gemeinsamkeiten
Die Typsysteme der fünf Sprachen stimmen in einer Reihe von Eigenschaften überein.
Typbindung: Jede Variable, jeder Parameter, jeder Ausdruck besitzt einen zur
Kompilationszeit festgelegten, unveränderlichen Typ – den statischen Typ.
Klassen und Typen: Das Klassenkonzept ist in das Typkonzept integriert. Jede
Klasse beschreibt einen Typ. (Eine generische Klasse beschreibt eine Menge von
Typen.)
Es gibt sprachdefinierte und programmiererdefinierte Typen.
Statische Typprüfung: Der Kompilierer prüft Typverträglichkeit in Ausdrücken,
bei Zuweisungen, Parameterübergaben usw.
Bei numerischen Basistypen gibt es Regeln zur impliziten Typanpassung.
Größen, die sich auf Objekte (Exemplare von Klassen) beziehen, haben auch einen
dynamischen Typ.
Konformität: Der dynamische Typ einer Objekt-Größe muss einer Unterklasse
ihres statischen Typs entsprechen.
7.2
Unterschiede
Die Typsysteme der fünf Sprachen unterscheiden sich hinsichtlich der Antworten auf
folgende Fragen.
Gibt es Typen, die keiner Klasse entsprechen?
Werden sprachdefinierte und programmiererdefinierte Typen völlig gleich behandelt? Wenn nein, welche Unterschiede gibt es?
Wann ist ein Typ konform zu, äquivalent, kompatibel oder verträglich mit einem
anderen?
Wie lasch oder streng ist die statische Typprüfung?
Welche Typen werden implizit angepasst? Nach welchen Regeln?
Gibt es Möglichkeiten zur expliziten Typanpassung?
Kann die Einhaltung von Wertebereichen zur Laufzeit überprüft werden?
Bei welchen Größen macht es Sinn, von ihrem dynamischen Typ zu sprechen?
Welche Typinformationen liegen zur Laufzeit vor?
Welche Möglichkeiten für Typprüfungen zur Laufzeit gibt es?
7.3
Typsystem
Component Pascal,
C++
Klassen beschreiben spezielle Typen. Es gibt Typen, die nicht durch Klassen beschrieben sind. Die Typ-Spezialisierungsstruktur ist typisch für hybride Sprachen.
Typen
sprachdefinierte Typen (Component Pascal: predefined, C++: built-in), einfache
Basistypen
programmiererdefinierte Typen
gewöhnliche Typen (Nicht-Klassen)
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 7 – Seite 1 von 18
7–2
7 Typen und Datenkonzepte
Eiffel
Klassen
Jeder Typ ist durch eine Klasse beschrieben. Die Typ-Spezialisierungsstruktur ist
typisch für rein objektorientierte Sprachen.
Java
Klassen
sprachdefinierte Klassen, Standardbibliotheksklassen
Basisklassen
programmiererdefinierte Klassen
Java zu ergänzen.
C#
C# zu ergänzen.
7.4
Sprachdefinierte Typen mit Operatoren
Wir geben jeweils untereinander die Typnamen, Beispiele für Konstanten dieser Typen,
und dazu definierte Operatoren an. Bei C* nennen wir nur nebeneffektfreie Operatoren.
Für alle Typen sind Tests auf Gleichheit und Ungleichheit definiert.
Tabelle 7.1 Vordefinierte Typen
Component
Pascal
Eiffel
C++
Java
C#
primitive type
simple type
Basistypen
basic type
basic class
=, #
=, /=
fundamental type
==, !=
boolesche Daten
BOOLEAN
BOOLEAN
FALSE, TRUE
False, True
~, &, OR
not, and then, or else,
xor, implies, and, or
bool
boolean
bool
false, true
!, &&, ||
und wie bei Integer
!, &&, ||,
<, <=, >, >=
!, &&, ||,
^, &, |
Zeichen
CHARACTER
SHORTCHAR
char kombinierbar
mit Qualifikator
signed oder
unsigned
CHAR
char
wchar_t
'a'
"b",
12X,
'%/12/',
'%N'
'\x12',
'\n'
'\u0012',
'\n'
<, <=, >, >=
-
und wie bei Integer
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
7.4 Sprachdefinierte Typen mit Operatoren
Component
Pascal
7–3
Eiffel
C++
Java
C#
natürliche Zahlen
unsigned char
byte
unsigned short int
ushort
unsigned int
uint
unsigned long int
ulong
0xAFFE
-
0xAFFE
-
<, <=, >, >=,
+, -, *, /,
/, %,
!, &&, ||, ?:
<, <=, >, >=,
+, -, *, /,
/, %,
!, &&, ||, ?:
(logic)
(logic)
~, &, |, ^, <<, >>
~, &, |, ^, <<, >>
(bit)
(bit)
ganze Zahlen
signed char
BYTE
byte
int kombinierbar
SHORTINT
INTEGER
short
mit Qualifikatoren
signed,
short oder long
INTEGER
LONGINT
int
long
123, -456
0xAFFE
0AFFEH
123_456_789
DIV, MOD
//, \\,
-
^ (power)
/, %,
!, &&, ||, ?: (logic)
~, &, |, ^, <<, >> (bit)
(unsigned
right shift)
>>>
-
-
Dezimalzahlen
-
decimal
Gleitpunktzahlen
SHORTREAL
REAL
float
float
32 Bit IEEE 754
REAL
DOUBLE
double
double
64 Bit IEEE 754
long double
1.0, 1.2E3
1.0F, 1.2E3F
1.0, 1.2E3
4.5D-6
4.5E-6
-
6.7E8L
<, <=, >, >=, +, -, *, /
27.9.12
^ (power)
!, &&, ||, ?:, &, |, ^ (logic, bit)
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7–4
7 Typen und Datenkonzepte
Component
Pascal
Eiffel
C++
Java
C#
Mengen
SET
kein Mengentyp, aber nachbildbar
mit Bitoperatoren auf
{1, 2, 3}
+, -, *, /
BIT-Typen
Bibliotheksklasse
BitSet
Integer, generischer Standardbibliotheksklasse
-
Bitset<n>
Abstraktion für halbgeordnete Mengen
PART_COMPARABLE,
-
COMPARABLE
Comparable
-
<, <=, >, >=
Abstraktion für algebraische Strukturen
-
NUMERIC
+, - , * , / , ^
Numeric
Bits
BIT N
mit SET
realisierbar
Bitoperatoren
not, and, or, xor,
implies, ^, #
mit Bitoperatoren auf Integer realisierbar
00100011B
Zeiger
-
POINTER
void*
-
void*
Nichtstypen
-
NONE
void
Die logischen, Zeichen- und Ganzzahltypen in Component Pascal und Eiffel haben disjunkte Wertebereiche. Dagegen sind die bool-, char- und int-Typen von C++ alle ganzzahlige Typen, sie werden in Ausdrücken ziemlich freizügig hin- und herkonvertiert.
Eiffel und C* bieten keinen Mengentyp, ein solcher kann mit einem Bitklassen- bzw.
Integertyp und Bitoperatoren simuliert werden.
Eiffel
COMPARABLE und NUMERIC sind abstrakte Klassen, die der Vererbung dienen.
POINTER hat keine Merkmale, die Klasse dient nur zur Beschreibung von Adressen,
mit denen ein Eiffel-Programm mit seiner Umgebung, d.h. Nicht-Eiffel-Programmen,
kommuniziert.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
decimal für
kommerzielle Anwendungen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
7.4 Sprachdefinierte Typen mit Operatoren
7–5
Component Pascal, C#
BOOLEAN, bool
Component Pascal,
Java, C#
Die Sprachen legen die Wertebereiche der sprachdefinierten Typen fest, sie hängen also
nicht von Kompilierern oder Prozessoren ab. Programme sind dadurch leichter portierbar.
27.9.12
haben nur die Werte false und true. Konversion in ganzzahlige
Werte ist nicht möglich.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Programmiererdefinierte Typen
7–6
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7.5
Tabelle 7.2 Programmiererdefinierte Typen
Art
Component Pascal
statische Reihung
ARRAY Anzahl OF Typ
Eiffel
Typ[Anzahl] Name
ARRAY [Typ]
Typ Name []
Typ[] Name
char Name []
dynamische
Reihung
POINTER TO
ARRAY OF Typ
Zeichenkette
ARRAY OF CHAR
Bibliotheksklasse
STRING
RECORD Attributliste END
als Klasse mit
Zugriffsroutinen
realisierbar
Zeiger
Routine
Aufzählung
Bibliotheksklasse String
Bibliotheksklasse string
struct Name
{ Attributliste }
class Name
{ Elementliste }
union Name
{ Attributliste }
mit Vererbung realisierbar
class Name
{ Elementliste }
mit Vererbung
realisierbar
Pointer: Typ * Name
POINTER TO Typ
PROCEDURE
(Formalparameterliste) :
Typ
C#
Typ Name [Anzahl]
ARRAY OF Typ
varianter Verbund
Java
generische
Bibliotheksklasse
offene Reihung
Verbund
C++
-
mit Konstanten realisierbar
Reference: Typ & Name
class Name
{ Elementliste }
Attribute StructLayout
und FieldOffset
Pointer: Typ * Name
-
Typ Name
(Formalparameterliste)
mit Muster realisierbar
enum Name
{ Symbolliste }
27.9.12
7 Typen und Datenkonzepte
enum Name
{ Symbolliste }
struct Name
{ Attributliste }
7.6 Typdeklarationen, Typdefinitionen
7–7
Eiffel
Es gibt nur das Klassenkonstrukt, um Typen zu konstruieren.
C++
Die Attribute eines struct sind bitweise implementierbar.
Die Attribute einer union werden auf dieselben Speicherzellen gelegt.
Eine Referenz entspricht einem konstanten Zeiger und wird implizit dereferenziert.
Der void-Typ wird hauptsächlich als Ergebnistyp von Funktionen gebraucht, die Prozeduren sein sollen. Von void gibt es keine Exemplare, da er keine Werte hat.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.5.1
Operatoren
Component Pascal
Programmiererdefinierte Typen sind Typen zweiter Klasse: Operatoren sind nämlich
für sie nicht durch den Programmierer definierbar.
Eiffel, C++
Programmierer- und sprachdefinierte Typen werden ziemlich gleich behandelt.
Eiffel
In Eiffel sind Operatoren syntaktische Varianten von Funktionen, die in einer Klasse
definiert oder redefiniert werden. Für programmiererdefinierte Typen können Operatoren mit beliebigen Sonderzeichen als Namen in Präfix- und Infixschreibweise – wie für
sprachdefinierte Typen – definiert werden. Konsistente Semantik von Operatoren wird
über Abstraktionen und Vererbungsbeziehungen erreicht.
C++
C++ nutzt für programmiererdefinierte Operatoren vor allem den Mechanismus des
Überladens. Nur Operatorsymbole, die schon in der Sprache definiert sind, können
gewählt werden. Operatoren können global oder an Klassen gebunden sein. Konsistente Semantik von Operatoren ist eher der Disziplin der Programmierer überlassen.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.6
Typdeklarationen, Typdefinitionen
Component Pascal
Es gibt anonyme und benannte Typen. Eine Typdeklaration führt einen neuen Typ mit
neuem Namen ein und hat die Form
TYPE Typname = Typangabe;
Typangaben bilden eine syntaktische Einheit. Typ- und Variablennamen werden vorangestellt, durch = bzw. : getrennt.
Eiffel
Die Namen sprachdefinierter Typen können mit anderer Bedeutung belegt werden.
Es gibt keine Typdefinitionen im Sinne von Component Pascal oder C++. Jede Klasse
erhält in ihrer Deklaration einen Namen. Typdeklarationen gibt es in folgenden Arten:
-- Normalfall Klasse = Typ
Klassenname [Klassenname]
-- von generischer Klasse abgeleiteter Typ
formaler generischer Klassenname
-- Parameter einer generischen Klasse
like Objektname
-- verankerter Typ
BIT N
-- Bittyp
Es gibt anonyme und benannte Typen. Eine Typdefinition gibt einem Typ einen neuen
Namen, sie führt aber keinen neuen Typ ein und hat näherungsweise die Form
Klassenname
C++
typedef Typangabe_Teil_1 Typname Typangabe_Teil_2;
Syntaktisch kombinieren Typangaben Elemente von Präfix- und Postfixnotationen.
Manche Teile einer Typangabe stehen vor, manche nach einem Typ- oder Variablennamen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7–8
7 Typen und Datenkonzepte
Die Namen sprachdefinierter Typen sind reservierte Wortsymbole.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.7
Typgleichheit, Typäquivalenz
Component Pascal,
Eiffel
Typgleichheit ist im wesentlichen als Namensäquivalenz definiert.
C++
Typäquivalenz bedeutet bei structs Namensäquivalenz.
Dagegen gelten die von einem Typ Typ abgeleiteten Zeiger-, Referenz-, Reihungs- und
Funktionstypen
Typ *, Typ &, Typ [], Typ ()
weniger als eigenständige Typen denn als eben abgeleitete Typen. Typdefinitionen
dafür sind zwar möglich, aber eher unüblich, da für sie auch nur strukturelle Äquivalenz, nicht Namensäquivalenz gilt.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.8
Wertprüfungen zur Laufzeit
Bei allen fünf Sprachen ist Unter- und Überlauf von Wertebereichen zur Laufzeit i.A.
möglich. Alle Sprachen bis auf C++ prüfen die Einhaltung von Wertebereichen zur
Laufzeit, d.h. nur C++ ist in dieser Hinsicht nicht typsicher.
7.9
Implizite Typanpassung
Component Pascal
Numerische Typen werden in arithmetischen Ausdrücken, bei Zuweisungen und Parameterübergaben implizit vom kleineren zum größeren Typ angepasst, sodass keine
Information, höchstens Genauigkeit verlorengeht. Die Kompatibilitätsregeln beruhen
auf dem Typeinschluss
BYTE
⊆ SHORTINT ⊆ INTEGER ⊆ LONGINT ⊆ SHORTREAL ⊆ REAL.
Der Kompilierer prüft Typkompatibilität.
Eiffel
Numerische Typen werden in arithmetischen Ausdrücken, bei Zuweisungen und Parameterübergaben implizit vom leichteren zum schwereren Typ angepasst, sodass keine
Information, höchstens Genauigkeit verlorengeht. Die Konformitätsregeln beruhen auf
der Balancierungsregel
INTEGER ⊆ REAL ⊆ DOUBLE.
Der Kompilierer prüft Typkonformität.
C++
Alle Fundamentaltypen können in Ausdrücken, bei Zuweisungen und Parameterübergaben beliebig eingesetzt werden, sie werden implizit in den geforderten Typ ohne
Rücksicht auf Verlust an Genauigkeit oder Information konvertiert. Es gibt daher weder
Kompatibilitätsprüfungen des Kompilierers noch Laufzeitprüfungen.1
Java, C#
Wie bei Component Pascal und Eiffel wird implizit verlustlos vom kleineren zum größeren Typ angepasst.
1
Der Autor fragt sich, ob der Begriff „strongly typed language“ für eine Sprache angemessen
ist, die statt statischer Typprüfung Typkonversion einsetzt.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
7.10 Explizite Typanpassung, Typkonversion
7–9
7.10
Explizite Typanpassung, Typkonversion
Component Pascal,
Eiffel
Es gibt kein Sprachkonstrukt zur Konversion programmiererdefinierter Typen oder
Klassen. Der Programmierer kann natürlich selbst Konversionsfunktionen schreiben,
wenn solche benötigt werden.
Component Pascal
Zur expliziten Typanpassung bestimmter Basistypen gibt es die sprachdefinierten
Funktionen
CHR, ENTIER, LONG, ORD, SHORT.
Eiffel
Die Bibliotheksklasse BASIC_ROUTINES enthält für Basisklassen die Konversionsfunktionen
charcode, charconv, double_to_integer, double_to_real, real_to_integer.
C++
Die aus C übernommene Typecast-Notation zur Typkonversion kann auf beliebige
Typen, also auch auf Klassen angewandt werden. Außerdem gibt es dafür eine funktionale Notation.
Typecast-Notation:
Some_type * obj_some = (Some_type*) obj;
((Some_type *) obj)->feature_of_Some_type;
Funktionale Notation:
typedef Other_type * Other_ptr;
Other_ptr obj_other = Other_ptr(obj);
Other_ptr(obj)->feature_of_Other_type;
Man beachte, dass damit beliebige Konversionen möglich sind, ohne dass ein Typtest
durchgeführt wird.1 Wenn das Objekt nicht mit dem Zieltyp verträglich ist, können
beliebig seltsame und unerwünschte Effekte auftreten.
Leitlinie 7.1
C++: Vermeide
Typkonversionen
Vermeide Typkonversionen! Oft ist ihre Verwendung Ausdruck eines schwachen
Entwurfs oder schlechten Programmierstils.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
Bibliotheksklasse: System.Convert.
Typecasts mit C-Notation:
(Typ) Ausdruck
7.11
Statischer und dynamischer Typ
Eine Unterscheidung zwischen statischem und dynamischem Typ ist nur in Zusammenhang mit Vererbung relevant. Der statische Typ einer Größe ist der Typ, mit dem sie
(zur Kompilationszeit) vereinbart ist. Der dynamische Typ einer Größe ist der Typ
ihres Werts zur Laufzeit. Der dynamische Typ entscheidet, welche Version einer gerufenen Operation ausgeführt wird.
Component Pascal
Bei einer Zeigervariablen oder einem Referenzparameter eines Verbundtyps kann der
dynamische Typ eine Erweiterung des statischen Typs sein.
Eiffel
Bei Größen mit Referenzsemantik kann der dynamische Typ eine Erweiterung des statischen Typs sein.
1
Wir haben es hier mit einer „streng typisierten Sprache“ zu tun, die uns alle Mittel bietet, um
sämtliche Typprüfungen zu unterlaufen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7 – 10
C++
7 Typen und Datenkonzepte
Die Terminologie ist anders: „Ein Objekt einer abgeleiteten Klasse kann als Objekt
ihrer Basisklasse behandelt werden, wenn es mit einem Zeiger manipuliert wird“
[Str91] S. 184. Statt Zeiger erfüllen auch Referenzen diesen Zweck.
Im Unterschied zu Component Pascal und Eiffel besitzt die Größe in C++ nicht einen
dynamischen Typ, sondern wird vom statischen Typ in diesen konvertiert.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.12
Typinformationen zur Laufzeit
Component Pascal
Es gibt das Konstrukt Typtest.
IF obj IS SomeType THEN
...
ELSIF obj IS OtherType THEN
...
END;
Eiffel
Viele Probleme lassen sich mit dem Mechanismus verankerter Typen (anchored types)
und der Kovarianzregel ohne Typtests lösen.
Die gemeinsame Oberklasse ANY und damit jede Klasse enthält das Merkmal
generator : STRING
das den Klassennamen als Zeichenkette liefert. Das Beispiel
if obj.generator.is_equal ("SOME_TYPE") then
...
elseif obj.generator.is_equal ("OTHER_TYPE") then
...
end
ist freilich nicht eleganter als das C++-Beispiel weiter unten.
Den Typvergleich zweier Objekte erlaubt das in ANY definierte Merkmal
conforms_to (other : like Current) : BOOLEAN
bei dem ein verankerter Typ verwendet wird. Anwendungsbeispiel:
if obj_some.conforms_to (obj_other) then ...
C++
Der Operator typeid, der der Abfrage des dynamischen Typs eines Objekts zur Laufzeit
dient, gehört zu den jüngsten Elementen des Sprachstandards.
if (typeid(obj) == typeid(SomeType)) {
...
} else if (typeid(obj) == typeid(OtherType)) {
...
};
Der Klassenname steht auch als Zeichenkette bereit. Das Beispiel
if (strcmp(typeid(obj).name(), typeid(SomeType).name())) ...
zeigt eine nicht gerade elegante Anwendung.
In der Menge der Typen gibt es eine totale Ordnung, bezüglich der zwei Objekte verglichen werden können:
if (typeid(obj_some).before(typeid(obj_other))) ...
Es ist aber nicht garantiert, dass diese Ordnungsrelation etwas mit der Vererbungsrelation zu tun hat! Der Nutzen dürfte daher begrenzt sein.
Java
Java zu ergänzen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
7.13 Typprüfungen zur Laufzeit
C#
7 – 11
C# zu ergänzen.
typeof
is
7.13
Typprüfungen zur Laufzeit
Component Pascal
Es gibt die Konstrukte Typzusicherung und bewachte Anweisung.
Typzusicherung:
obj(SomeType).featureOfSomeType;
Bewachte Anweisung:
WITH obj : SomeType DO
obj.featureOfSomeType;
| obj : OtherType DO
obj.featureOfOtherType;
ELSE
...
END;
Eiffel
Für die Zuweisung eines Objekts unbekannten Typs gibt es den Zuweisungsversuch
(assignment attempt).
obj_some : SOME_TYPE
obj_other : OTHER_TYPE
obj_some ?= obj
obj_other ?= obj
if obj_some /= Void then
obj_some.feature_of_SOME_TYPE
elseif obj_other /= Void then
obj_other.feature_of_OTHER_TYPE
else
...
end
C++
Statt Typprüfungen gibt es Typkonversionen, siehe 7.14 S. 11.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
as
7.14
Typkonversionen zur Laufzeit
Component Pascal,
Eiffel
Typkonversionen zur Laufzeit sind nicht möglich (und nicht notwendig).
C++
Konstrukte zur Konversion eines Objekts von einem Typ in einen anderen zur Laufzeit
wurden spät in den Sprachstandard aufgenommen. Sie beinhalten die Operatoren
static_cast
reinterpret_cast
const_cast
dynamic_cast
In [Str91] und [ElS90] sind sie auf eine Weise erklärt, die für einen durchschnittlich
begabten Menschen des europäischen Kulturkreises nicht ganz leicht zu verstehen ist.
Das Problem wird in der Literatur breit diskutiert.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7 – 12
7 Typen und Datenkonzepte
7.15
Zeiger
Component Pascal
Zeiger beziehen sich nur auf dynamische Variable eines Verbundtyps oder eine offene
Reihung (die auf der Halde liegen).
Referenzierung:
TYPE Some = RECORD ... END;
VAR ptr : POINTER TO Some;
Dereferenzierung:
some := ptr^;
Es gibt den speziellen Wert NIL für den Bezug auf nichts.
Eiffel
Es gibt keine spezielle Notation für Zeiger, da sie nicht erforderlich ist. Variablen, Parameter usw. sind i.d.R. Bezüge auf Objekte. Es wird implizit dereferenziert.
C++
Zeiger beziehen sich auf statische oder dynamische Variablen beliebigen Typs. Sie können sich auf beliebige Speicheradressen beziehen.
Referenzierung:
int * ptr, i;
Adresszuweisung:
ptr = &i;
Dereferenzierung:
i = *ptr;
Als spezieller Wert für den Nichtsbezug dient 0, NULL oder '\0'.
Es gibt eine spezielle Zeigerarithmetik (Addition, Subtraktion).
Außerdem gibt es Referenzen. Das sind konstante Zeiger, entsprechen also etwa dem
Pseudokonstrukt CONSTANT POINTER TO ...
Die Adresszuweisung erfolgt im Initialisierungsteil der Definition ohne expliziten
Adressoperator:
int i = 123;
int & ref = i;
Die Dereferenzierung erfolgt implizit:
ref = 123;
Referenzen sind vor allem als Referenzparameter nützlich.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.16
Reihungen
Component Pascal
Reihungen werden statisch vereinbart. Die Elementzahl ist durch eine positive ganze
Zahl gegeben, der Indexbereich beginnt immer bei 0, der Elementtyp ist beliebig.
VAR a : ARRAY 10 OF REAL;
Der Wert der Variablen a ist das Feld.
Zugriff auf die Elemente mit indizierten Variablen:
a [0], a [9]
Die Überschreitung von Bereichsgrenzen wird zur Laufzeit überprüft.
Mehrdimensionale Reihungen sind möglich.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
7.16 Reihungen
7 – 13
VAR b : ARRAY 8, 7 OF REAL;
b [1][2] und b [1, 2] sind
äquivalent.
Als formale Parameter sind offene Reihungen (ohne Angabe der Elementzahl) erlaubt.
Reihungen können dynamisch erzeugt werden, wenn ein Zeiger auf eine offene Reihung vereinbart ist.
VAR openArr : POINTER TO ARRAY OF REAL;
...
NEW (openArr, 64);
Eiffel
Reihungen werden als Objekte einer Bibliotheksklasse dynamisch erzeugt. Der Indexbereich ist durch zwei ganzzahlige Werte festgelegt, der Elementtyp ist beliebig.
a : ARRAY [REAL]
Der Wert der Variablen a ist ein Bezug auf ein Objekt des Typs ARRAY [REAL], der von
der generischen Klasse ARRAY abgeleitet ist.
Erzeugung eines Objekts:
create a.make (0, 9);
Zugriff auf die Elemente erfolgt ausschließlich über Zugriffsroutinen:
a.put (0, x)
x := a.item (9)
Für den Lesezugriff gibt es auch eine Infix-Notation:
x := a @ 9
Die Überschreitung von Bereichsgrenzen wird durch Zusicherungen (Vorbedingungen,
Invarianten) geprüft.
Mehrdimensionale Reihungen sind durch geschachtelte Vereinbarungen
b : ARRAY [ARRAY [REAL]]
oder spezielle Klassen möglich.
C++
Eigentlich gibt es in C/C++ keine Reihungen, sondern nur eine Reihungsschreibweise
für Zeiger.
Die Elementzahl ist durch eine positive ganze Zahl gegeben, der Indexbereich beginnt
immer bei 0, der Elementtyp ist beliebig.
float a [10];
Der Wert der Variablen a ist ein Bezug auf das Feld.
Zugriff auf die Elemente mit indizierten Variablen oder Zeigerarithmetik:
a[0] ist äquivalent mit *a
a[9] ist äquivalent mit *(a + 9)
Die Überprüfung der Überschreitung von Bereichsgrenzen ist eine seltene Kompiliereroption. Die Bereichsgrenzen sind zudem zur Laufzeit nicht bekannt! Sie müssen daher
bei Bedarf in zusätzlichen Variablen mitgeführt werden.
Mehrdimensionale Reihungen sind möglich, allerdings muss der Programmierer bei
Parameterübergabe die Speicherabbildungsfunktion beherrschen.
float b [length * width];
b[1][2] und b[1, 2] sind nicht äquivalent (aber beide legal).
Reihungen können dynamisch erzeugt werden.
Reihungsschreibweise:
27.9.12
Zeigerschreibweise:
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7 – 14
7 Typen und Datenkonzepte
float open_arr [];
...
open_arr = new float[64];
float * open_arr;
Man beachte, dass dynamisch erzeugte Variable explizit gelöscht werden müssen, da
C++ keine automatische Speicherbereinigung bietet.
Es empfiehlt sich, in C++ auf die von C übernommenen Reihungen sowie die Zeigerarithmetik weitgehend zu verzichten und stattdessen die generische vector-Bibliotheksklasse der STL zu verwenden, da diese sicherer ist.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.17
Zeichenketten
Component Pascal
Zeichenketten sind Reihungen von Zeichen. Sie werden mit dem Zeichen 0X abgeschlossen.
VAR s : ARRAY 10 OF CHAR;
Es gibt ein Bibliotheksmodul Strings mit Prozeduren zur Zeichenkettenverarbeitung.
Eiffel
Zeichenketten werden als Objekte einer Bibliotheksklasse dynamisch erzeugt. Die
Länge ist durch einen ganzzahligen Wert festgelegt.
s : STRING
Der Wert der Größe s ist ein Bezug auf ein Objekt der Klasse STRING.
Erzeugung eines Objekts:
create s.make (10)
Die Klasse enthält viele Routinen zur Zeichenkettenverarbeitung.
C++
Zeichenketten sind als Zeiger auf Zeichen oder als Reihungen von Zeichen darstellbar.
char * s;
char t [10];
Eine Zeichenkette wird durch das Zeichen 0 oder '\0' abgeschlossen. Dies erlaubt eine
effiziente Speicherung.
Die Überschreitung von Bereichsgrenzen wird i.A. nicht geprüft (kein Laufzeitfehler),
sondern führt zu fehlerhaften Zugriffen auf fremde Speicherbereiche.
Es gibt eine umfangreiche ANSI-C-Bibliothek (string.h) mit Funktionen zur Zeichenkettenverarbeitung sowie eine C++-Bibliotheksklasse string, deren Benutzung vorzuziehen ist.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.18
Verbunde
Component Pascal
Die Typen der Attribute sind beliebig.
VAR
r:
RECORD
name : ARRAY 20 OF CHAR;
age
: INTEGER;
salary : REAL;
END;
Zugriff auf die Elemente mit Punktschreibweise:
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
7.19 Mengen
7 – 15
r.name, r.age, r.salary
Verbunde sind nicht (durch den Programmierer) implementierbar.
Eiffel
Verbunde können als Klassen realisiert werden. Die Attribute sind von außen nicht
zugreifbar. Schreibzugriffe müssen mit Zugriffsroutinen realisiert werden. Dies entspricht dem Konzept der Datenabstraktion.
C++
Die Typen der Attribute sind beliebig.
struct {
char
int
float
} r;
name [20];
age;
salary;
Zugriff auf die Elemente mit Punktschreibweise:
r.name, r.age, r.salary
Verbunde sind implementierbar.
struct Some_byte {
int bits0to2
: 3;
int
: 2;
int bits5to7
: 3;
};
// unused
Das Konstrukt ist trotzdem nicht portierbar, da die tatsächliche Implementation vom
Kompilierer abhängt.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.19
Mengen
Component Pascal
Die Elemente einer Menge sind ganze Zahlen aus einem kleinen Wertebereich (0 .. 31).
VAR s : SET;
Es gibt Mengenoperatoren und -prozeduren.
Eiffel
Mengen mit beliebigem Elementtyp werden als Objekte einer generischen Bibliotheksklasse dynamisch erzeugt. Der Elementtyp ist eine beliebige Klasse.
s : SET [INTEGER];
Die Klasse enthält viele Routinen, darunter Mengenoperatoren und Zugriffsroutinen
auf Elemente.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.20
Einfache Typen
7.20.1
Zeichen und Zahlen
Component Pascal
Zeichen und Zahlen sind unterscheidbare Typen mit unterschiedlichen Operatoren. Die
Anpassung erfolgt explizit mit Standardfunktionen.
VAR c : CHAR; i : INTEGER;
c := 'A';
c := CHR (65);
i := 65;
i := ORD (0AX);
i := ORD ('A') + ORD ('B');
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7 – 16
Eiffel
7 Typen und Datenkonzepte
Eiffel bietet konzeptuell dasselbe wie Component Pascal, nur mit den Funktionen charconv und charcode der Klasse BASIC_ROUTINES statt CHR und ORD.
C++
Zeichen und Zahlen werden kaum unterschieden. Auf Zeichen sind arithmetische Operationen anwendbar.
char c; int i;
c = 'A';
c = 65;
c = '\65';
i = 65;
i = '\xA';
i = 'A' + 'B';
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.20.2
Boolesche Daten
C
C kannte lange keinen booleschen Datentyp und keine logischen Ausdrücke in strengem Sinn. Der Wert 0 wurde mit false identifiziert, jeder andere Wert mit true. Dies
führte dazu, dass C-Programme mit untereinander inkompatiblen Definitionen der Art
#define BOOLEAN unsigned char
#define FALSE 0
#define TRUE 1
enum Boolean {False, True};
typedef enum {false = 0, true = 1} BOOL;
angefüllt wurden, sowie zu einem heftigen Religionskrieg, ob denn in C++ ein boolescher Datentyp aufgenommen werden sollte oder nicht.
C++
Der Friedensvertrag wurde mit dem C++-Standard geschlossen, der mit bool einen
ganzzahligen Typ mit den Werten false und true bietet. Es wird freizügig implizit konvertiert, um Kompatibilität mit alten C-Programmen aufrechtzuerhalten: false nach 0
und zurück, true nach 1, jeder von 0 verschiedene Wert von Zahlen oder Zeigern nach
true.
C99
Schließlich erhielt C99 das Schlüsselwort _Bool, das einen zweiwertigen Zahlentyp mit
den Werten false und true bezeichnet. Zudem gibt es die C-Standardschnittstellendatei
stdbool.h mit Makros zu den Spezifikationen:
bool
false
true
expandiert zu
expandiert zu
expandiert zu
_Bool
0
1
Java
Java zu ergänzen.
C#
C# zu ergänzen.
7.20.3
Aufzählungen
Component Pascal
Die aus Pascal und Modula-2 bekannten Aufzählungstypen fielen in Component Pascal dem Prinzip der Minimierung der Sprachkonzepte zum Opfer. Aufzählungen müssen mit ganzzahligen Konstanten realisiert werden, wobei Typsicherheit verlorengeht.
CONST
red
green
blue
numberOfColours
= 0;
= 1;
= 2;
= 3;
VAR colour : INTEGER;
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
7.21 Konstanten
7 – 17
ASSERT ((0 <= colour) & (colour <= numberOfColours), BEC.invariant);
Eiffel
red, green, blue : INTEGER is unique
colour : INTEGER
invariant
colour = red or colour = green or colour = blue
Die den Bezeichnern red, green, blue zugeordneten Werte sind klassenweit eindeutige
konsekutive positive Ganzzahlen. Die Einhaltung des Wertebereichs wird hier mit einer
Invariante geprüft.
C++
Die Aufzählungstypen sind aus C übernommen.
enum Colour {red, green, blue};
Colour colour;
Aufzählungen sind mit ganzen Zahlen verträglich, d.h. es wird implizit konvertiert. Ob
bei einem Typkonflikt der Kompilierer eine Warnung liefert?
Ein Aufzählungstyp ist implementierbar.
enum Colour {red = 0, green = 9, blue = -1};
Java
Java zu ergänzen.
public class Colour {
public static final red = 0;
public static final green = 1;
public static final blue = 2;
}
public class ColourClient {
int colour = Colour.red;
}
C#
C# zu ergänzen.
7.21
Konstanten
Component Pascal
Beispiele für literale Konstanten:
'a', 1, 2.3, 4.5E-6, 7E8, 'cde', "f", "ghe"
Konstantennamen für einfache Werte sind in einer Konstantenvereinbarung definierbar.
Es handelt sich um Übersetzungszeitkonstanten.
CONST pi = 3.14;
Es gibt Hexadezimalkonstanten.
0AFFEH
Eiffel
Beispiele für literale Konstanten:
"a", 1, 2.3, 4.5E-6, 7E8, 9.E2, .3E-4, "cde"
Namen für Werte von Basistypen sind in einer Merkmalsvereinbarung einer Klasse
definierbar.
pi : REAL = 3.14;
In diesem Beispiel ist eine Einmalfunktion günstig einsetzbar.
pi : REAL
once
Algorithmus zur Berechnung von pi
Result := Ausdruck
end
Kommt es auf den Wert nicht an, kann das Wortsymbol unique verwendet werden.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
7 – 18
7 Typen und Datenkonzepte
left, right : INTEGER is unique;
Einmalfunktionen können auch für konstante Objekte und zur Bildung von Laufzeitkonstanten verwendet werden.
C++
Jede literale Zahlenkonstante in Component Pascal ist auch eine in C++, aber nicht
umgekehrt. Beispiele für literale Konstanten:
'a', 1, 2.3, 4.5E-6, 7E8, 9.E2, .3E-4, "cde"
Oktalzahlen:
012
Hexadezimalzahlen:
0xAFFE
Der Typ kann durch Anhängen von U, L oder F festgelegt werden (kleine e, u, l, f tun’s
auch).
0x123U, 456L, 7.8F, 9.1E2L
C kennt keine Konstantennamen. Solche sind aber mit einer Vorübersetzerdirektive
definierbar. Sie werden vor der Kompilation substituiert.
#define pi (3.14)
C++ bietet aber richtige Konstanten, deren Benutzung man vorziehen sollte.
const float pi = 3.14;
Java
Java zu ergänzen.
public static final float pi = 3.14;
C#
C# zu ergänzen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
8
Ablaufsteuerung und algorithmische Konzepte
Aufgabe 8
8.1
Routinen
Unter Routinen verstehen wir Prozeduren und Funktionen. Die syntaktischen Einheiten zur Vereinbarung von Routinen sind in jeder Sprache anders bezeichnet.
Tabelle 8.1
Routinen
Component Pascal
Eiffel
Routine
Prozedur
Routine
Funktion
Prozedur
gewöhnliche Prozedur
Kommando
void-Funktion
Funktion
Funktion, Funktionsprozedur
Funktion
nicht-void-Funktion
8.1.1
Prozeduren
Component Pascal
Prozedurdeklaration:
C++
Java
C#
PROCEDURE Prozedurname (Formalparameterliste);
lokale Vereinbarungen
BEGIN
Anweisungen
END Prozedurname;
Der Prozeduraufruf ist eine Anweisung:
Prozedurname (Aktualparameterliste);
Eiffel
Spezielle Merkmalsdeklaration:
Prozedurname (Formalparameterliste)
-- Kurzbeschreibung
require
Vorbedingungen
local
lokale Vereinbarungen
do
Anweisungen
ensure
Nachbedingungen
end
Zur Spezifikation von Routinen mit Vor- und Nachbedingungen sind spezielle (optionale) Zusicherungsabschnitte eingebaut.
Der spezielle Merkmalsaufruf ist eine Anweisung:
Prozedurname (Aktualparameterliste)
C++, Java, C#
Funktionsdefinition:
void Prozedurname (Formalparameterliste) {
lokale Vereinbarungen
Anweisungen
};
Eine Prozedur ist eine Funktion, deren Ergebnis vom „Nichtstyp“ void ist. Man beachte,
dass in C++ p (...) nicht eine Prozedur, sondern äquivalent zu int p (...), also eine Funktion mit Ergebnistyp int ist.
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 8 – Seite 1 von 20
8–2
8 Ablaufsteuerung und algorithmische Konzepte
Vereinbarungen sind spezielle Anweisungen und treten daher innerhalb von Verbundanweisungen auf.
Der Funktionsaufruf ist ein Ausdruck, der als Ausdrucksanweisung hingeschrieben
wird:
Prozedurname (Aktualparameterliste);
8.1.2
Funktionen
Component Pascal
Funktionsdeklaration:
PROCEDURE Funktionsname (Formalparameterliste) : Ergebnistyp;
lokale Vereinbarungen
BEGIN
Anweisungen
RETURN Ausdruck vom Ergebnistyp;
END Funktionsname;
Der Funktionsaufruf ist ein Ausdruck
Funktionsname (Aktualparameterliste)
dessen Wert weiter verarbeitet werden muss. Er kann syntaktisch keine Anweisung
sein.
Eiffel
Spezielle Merkmalsdeklaration:
Funktionsname (Formalparameterliste) : Ergebnistyp
-- Kurzbeschreibung
require
Vorbedingungen
local
lokale Vereinbarungen
do
Anweisungen
Result := Ausdruck vom Ergebnistyp
ensure
Nachbedingungen
end
Result ist eine implizit vereinbarte lokale Größe vom Ergebnistyp. Das Ergebnis muss
einen Namen haben, damit man es in Nachbedingungen verwenden kann.
Der spezielle Merkmalsaufruf ist ein Ausdruck
Funktionsname (Aktualparameterliste)
dessen Wert weiter verarbeitet werden muss. Er kann syntaktisch keine Anweisung
sein.
C++, Java, C#
Funktionsdefinition:
Ergebnistyp Funktionsname (Formalparameterliste) {
lokale Vereinbarungen
Anweisungen
return Ausdruck vom Ergebnistyp;
};
Der Funktionsaufruf ist ein Ausdruck
Funktionsname (Aktualparameterliste)
dessen Wert weiter verarbeitet werden muss. Als Ausdrucksanweisung hingeschrieben
ist er nur sinnvoll (aber problematisch), wenn die Funktion einen Nebeneffekt produziert:
Funktionsname (Aktualparameterliste);
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
Parameter- und Ergebnisübergabe
Tabelle 8.2 Parameter- und Ergebnisübergabe
Eigenschaft
Component Pascal
Formalparameterliste
Eiffel
C++
Java
par1 : Type1; par2 : Type2
Type1 par1, Type2 par2
par1, par2 : Type
-
Abkürzung bei gleicher Art
und gleichem Typ
C#
8.1 Routinen
27.9.12
8.1.3
Leeres Klammernpaar () bei Vereinbarung und Aufruf einer parameterlosen Routine erforderlich?
Prozedur
ohne
Funktion
mit
Referenzübergabe
mit
Type par_val
parVal : Type
ja
Eingabereferenz
IN parRef : StructuredType
Ausgabereferenz
OUT parRef : Type
Ein-/Ausgabe-Referenz
VAR parRef : Type
kein Konstrukt vorhanden, aber
übergebene Größen sind
meist Referenzobjekte
const Type & par_ref
const Type * par_ptr
ja
übergebene Größen sind
meist Referenzobjekte
möglich durch Wertübergabe
-
einer Referenz: Type & par_ref
oder eines Zeigers: Type * par_ptr
Ergebnisübergabe
mit spezieller Sprunganweisung RETURN
mit implizit definierter
Größe Result
Zuweisung an Formalparameter
erlaubt (außer bei IN)
verboten
out Type parRef
ref Type parRef
mit spezieller Sprunganweisung return
erlaubt (außer bei const)
erlaubt
8–3
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Wertübergabe
ohne
8–4
8 Ablaufsteuerung und algorithmische Konzepte
C++
f () stellt in C++ eine parameterlose Funktion dar, in C dagegen eine Funktion mit
beliebig vielen Argumenten beliebigen Typs. Eine parameterlose Funktion muss in C
als f (void) vereinbart werden.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
Variable Parameteranzahl.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
8.1 Routinen
8.1.4
8–5
Eigenschaften von Routinen
Tabelle 8.3 Eigenschaften von Routinen
Eigenschaft
Component
Pascal
Eiffel
Routinenaufruf
Prozeduraufruf ist
C++
Java
C#
Routinenname (Aktualparameterliste)
Anweisung
(statement)
Anweisung
(instruction)
Ausdruck (expression)
Funktionsaufruf ist
während Ausführung der Routine
Existenzdauer
lokaler Größen
falls mit static
spezifiziert:
über Aufrufe
hinweg
-
von innen nach außen
lokale Größen verdecken globale Größen
Merkmale und
Parameter
dürfen nicht
gleiche
Namen haben
Sichtbarkeitsregeln
-
Aliasing mit globalen Größen und
Formalparametern
möglich
möglich
Nebeneffekte bei
Funktionen
vermeidbar
Routinentypen
ja
Schachtelung
ja
kaum vermeidbar
nein
Maßnahme bei
indirekter
Rekursion
ja
nein
Rekursion
Vereinbarung vor
Aufruf erforderlich
-
ja
ja
Vorwärtsdeklaration
PROCEDURE
R^ (Par...) : Typ;
nein
ja
-
Funktionsdeklaration
Typ r (Typen);
C++
In C++ ist der Aufruf einer nicht deklarierten Funktion verboten, während er in C
erlaubt ist. Damit wurde eine Fehlerquelle von C – fehlende Typprüfung – in C++
beseitigt.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
8–6
8.1.5
Component Pascal
8 Ablaufsteuerung und algorithmische Konzepte
Routinentypen und -variablen
TYPE FT = PROCEDURE (x : REAL) : REAL;
VAR F : FT;
PROCEDURE Sqr (x : REAL) : REAL;
BEGIN
RETURN x * x;
END Sqr;
...
F := Sqr;
F (1.23);
Eiffel
Auf Routinentypen wird verzichtet, weil jeder Typ auf einer Klasse beruht, Routinen zu
Klassen gehören und die Zwecke, denen Routinentypen in prozeduralen Programmiersprachen dienen, in der objektorientierten Programmierung mit Klassen erfüllbar sind.
deferred class FT
feature
f (x : REAL) : REAL
deferred
end
end -- class FT
class SQR
inherit
FT
redefine
f
end
feature
f (x : REAL) : REAL
do
Result := x * x
end
end -- class SQR
...
f : FT
sqr : SQR
...
C++
Mit eigenem sqr-Attribut:
Ohne eigenes sqr-Attribut:
create sqr;
f := sqr;
f.f (1.23);
create {SQR} f;
f.f (4.56);
Mit Typdefinition:
Ohne Typdefinition:
typedef float (*FT) (float);
FT f;
float (*f) (float);
float sqr (float x) {
return x * x;
};
...
f = &sqr;
(*f)(1.23);
f(4.56);
// explizite Referenzierung
// explizite Dereferenzierung
// implizite Dereferenzierung
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
8.1 Routinen
Java
8–7
public abstract class FT {
public abstract float f (float x);
}
public class Sqr extends FT {
public float f (float x) {;
return x * x;
}
}
Mit eigenem Sqr-Attribut:
Ohne eigenes Sqr-Attribut:
Sqr sqr = new Sqr();
FT f = sqr;
f.f (1.23);
FT f = new Sqr();
f.f (4.56);
C#
C# zu ergänzen.
8.1.6
Was entspricht den vordeklarierten Component-Pascal-Prozeduren?
Abkürzungen:
B.
B.K.
F.
P.
27.9.12
Bibliothek
Bibliotheksklasse
Funktion
Prozedur
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Component Pascal
Eiffel
ASSERT (x : BOOLEAN)
Anweisung
ASSERT (x : BOOLEAN,
n : Integerkonstante)
check Etikett :
boolescher Ausdruck end
DEC (v : Integer)
DEC (v, n : Integer)
INC (v, n : Integer)
NEW (v : Zeiger auf Verbund
oder festes Array)
NEW (v : Zeiger auf offenes
Array; x0,...,xn : Integer)
assert
boolescher Ausdruck :
"Text";
-
B. assert.h, F.n
void assert(int)
void Assert
(Ausdruck, Ausnahme)
v := v - 1
--v; v--
v := v - n
v -= n
kein Mengentyp, aber nachbildbar mit
BIT-Typen mit Operatoren
not, and, or, xor, implies, ^, #
B.K. EXCEPTIONS,
P. raise (STRING)
Bit-Operatoren
~, &, |, ^, <<, >> auf Integer
B. stdlib.h, F.n
void exit (int),
void abort ()
Anweisung
Ausdruck
v := v + 1
++v; v++
v := v + n
v += n
Erzeugungsanweisung
Erzeugungsoperator
Erzeugungsoperator
v : Typ
create v.Prozedur (...)
Typ * v = new Typ;
Typ v = new Typ(...);
Typ * v = new Typ [len];
Typ[] v = new Typ(...)[len];
27.9.12
8 Ablaufsteuerung und algorithmische Konzepte
INC (v : Integer)
C#
Ausdruck
B.K. ANY, P. die (INTEGER)
HALT (n : Integerkonstante)
Java
Anweisung
EXCL (v : SET; x : Integer)
INCL (v : SET; x : Integer)
C++
8–8
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Tabelle 8.4 Was entspricht den vordeklarierten Component-Pascal-Prozeduren?
Was entspricht den vordeklarierten Component-Pascal-Funktionen?
Tabelle 8.5 Was entspricht den vordeklarierten Component-Pascal-Funktionen?
Component Pascal
Eiffel
C++
Java
C#
8.1 Routinen
27.9.12
8.1.7
B.n stdlib.h, math.h, F.n
ABS (x : Numeric) : Typ von x
B.K. BASIC_ROUTINES, F.
int abs (int)
abs (INTEGER) : INTEGER
long labs (long)
double fabs (double)
Schiebeoperatoren
ASH (x, n : Integer) : LONGINT
x : BIT N; n : INTEGER
x^n
Integer x; Unsigned n
x << n; x >> n
CAP (c : CHAR) : CHAR
B.K. STRING, P. to_upper
B. ctype.h, F. int toupper (int)
B.K. BASIC_ROUTINES, F.
CHR (x : Integer) : CHAR
charconv (INTEGER) :
CHARACTER
B.K. BASIC_ROUTINES, F.n
ENTIER (x : Real) : LONGINT
real_to_integer (REAL) :
INTEGER
double_to_integer (DOUBLE) :
INTEGER
LEN (a : Array) : LONGINT
B.K. ARRAY [Typ], F.n
LEN (a : Array; n : Integerkonstante.) : LONGINT
capacity, count, lower, upper :
INTEGER
implizite Konversion, Zeichen sind Zahlen
implizite Konversion
B. math.h, F.
double floor (double)
Länge einer Reihung zur
Laufzeit nicht abrufbar
LONG (x : BYTE) : SHORTINT
LONG (x : SHORTINT) :
INTEGER
LONG (x : INTEGER) :
LONGINT
LONG (x : SHORTREAL) :
REAL
implizite Konversion
implizite oder
explizite Konversion
mit Typecast
System.Convert
8–9
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Schiebeoperator
Eiffel
C++
MAX (Grundtyp) : Grundtyp
-
B. limits.h, Makros
Java
C#
8 – 10
CHAR_MAX, SCHAR_MAX,
UCHAR_MAX, SHRT_MAX,
USHRT_MAX, INT_MAX,
UINT_MAX, LONG_MAX,
ULONG_MAX, FLT_MAX,
DBL_MAX, LDBL_MAX
MAX (SET): INTEGER
-
-
MIN (Grundtyp) : Grundtyp
-
B. limits.h, Makros
CHAR_MIN, SCHAR_MIN,
UCHAR_MIN, SHRT_MIN,
USHRT_MIN, INT_MIN,
UINT_MIN, LONG_MIN,
ULONG_MIN, FLT_MIN,
DBL_MIN, LDBL_MIN
MIN (SET): INTEGER
ODD (x : Integer) : BOOLEAN
-
-
Ausdruck
Ausdruck
x \\ 2 /= 0
x % 2 != 0
B.K. BASIC_ROUTINES, F.
ORD (c : CHAR) : INTEGER
SHORT (x :SHORTINT) : BYTE
SHORT (x :INTEGER) :
SHORTINT
SHORT (x : LONGINT) :
INTEGER
SHORT (x : REAL) :
SHORTREAL
charcode (CHARACTER) :
INTEGER
implizite Konversion
B.K. BASIC_ROUTINES, F.n
double_to_real (DOUBLE) :
REAL
implizite oder
explizite Konversion
mit Typecast
System.Convert
27.9.12
8 Ablaufsteuerung und algorithmische Konzepte
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Component Pascal
SIZE (Typ) : Integer
Eiffel
C++
B.K. INTERNAL, F.
Operator
physical_size (ANY) :
INTEGER
Integer sizeof (Typ)
Integer sizeof Ausdruck
Java
-
C#
Operator
Integer sizeof (Typ)
8.1 Routinen
27.9.12
Component Pascal
8 – 11
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
8 – 12
8.2
8 Ablaufsteuerung und algorithmische Konzepte
Zusicherungen
Als Beispiel für die Anwendung von Zusicherungen siehe 2.3 S. 2-26. Zusicherungen
können z.B. dazu verwendet werden, die Einhaltung von Wertebereichen zur Laufzeit
zu überprüfen.
Component Pascal
Es gibt allgemeine Zusicherungen in Form vordeklarierter Prozeduren.
ASSERT (Bedingung);
ASSERT (Bedingung, Fehlernummer);
Ist die Bedingung falsch, so wird der Programmablauf mit einem Trap abgebrochen.
Zur Information werden die Fehlernummer, der Aufrufkeller mit allen Variablen und
die Abbruchstelle im Quellprogramm angezeigt. Die Prüfung der Zusicherungen ist
obligatorisch.
Eiffel
Ein wichtiges Entwurfsziel ist, dass sich die Sprache als Spezifikationssprache eignen
und die Entwicklung korrekter Programme unterstützen soll. Den Sprachkonstrukten
liegt das Kunden-Lieferanten-Modell (client-supplier-model) zugrunde, auf dem auch
eine Entwicklungsmethode aufbaut. Dabei werden Prozeduren mit Vor- und Nachbedingungen, Klassen mit Invarianten spezifiziert. Entsprechend bietet Eiffel folgende
Arten von Zusicherungen:
Allgemeine Zusicherung als Anweisung:
check Bedingung end;
Vor- und Nachbedingungen von Routinen als Abschnitte (siehe 8.1.1 S. 1):
require Vorbedingungen
ensure Nachbedingungen
Diese Abschnitte werden vererbt, d.h. eine Redefinition einer Routine muss sich an
die geerbte Spezifikation halten. Sie darf nur die Vorbedingungen abschwächen und
die Nachbedingungen verstärken.
Invarianten von Klassen als Abschnitt:
invariant Konsistenzbedingungen
Die Konsistenzbedingungen müssen vor und nach jedem Aufruf einer Routine gelten. Der Abschnitt wird vererbt, d.h. eine Unterklasse einer Klasse muss sich an die
geerbte Spezifikation halten. Sie darf die Invarianten nur verstärken.
Invarianten und Varianten von Schleifen als Abschnitte:
invariant Bedingung
variant nichtnegativ-ganzzahliger Ausdruck
Die require-, ensure- und invariant-Abschnitte sind in objektorientierte Konzepte integriert. Ist eine Prüfbedingung falsch, so wird eine sprachdefinierte Ausnahme (exception) ausgelöst. Diese Ausnahme kann an definierter Stelle auf programmiererdefinierte Weise behandelt werden. Ist keine Ausnahmebehandlung programmiert, so wird
der Programmablauf abgebrochen. Die Überprüfung von Zusicherungen ist selektiv zur
Laufzeit steuerbar.
C++
Zusicherungen sind durch eine Bibliothek mit der Schnittstellendatei assert.h unterstützt. Dort findet sich von ANSI-C das Vorübersetzermakro
assert(Bedingung);
Ist die Prüfbedingung falsch, so wird der Programmablauf durch Aufruf der Funktion
abort () abgebrochen. Zur Information werden der Wert der Bedingung (die ein beliebiger Ausdruck ist), der Name der Quelldatei und die Zeilennummer der Abbruchstelle
ausgegeben. Die Vorübersetzerdirektive
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
8.2 Zusicherungen
8 – 13
#define NDEBUG
schaltet die Überprüfung der Zusicherungen ab (vor der Kompilation).
C++ bietet zusätzlich die generische Funktion
Assert (Bedingung, Ausnahme);
Ist die Prüfbedingung falsch, so wird die übergebene programmiererdefinierte Ausnahme (exception) ausgelöst. Die Reaktion hängt dann von der Ausnahme und ihrer
Behandlung ab. Die Überprüfung dieser Zusicherungen ist auch mit NDEBUG steuerbar.
Mit Hilfe von Ausnahmen kann man auch eigene Prüffunktionen programmieren.
Java
Java bietet Zusicherungen erst seit Release 1.4 in Form eines Sprachkonstrukts.1 Zusicherungen werden mit dem Schlüsselwort assert eingeleitet, dem booleschen Ausdruck kann eine Zeichenkette folgen:
assert Bedingung : Zeichenkette;
Beim Aufruf des Java-Kompilierers ist anzugeben, dass er das Sprachkonstrukt akzeptieren soll:
javac -source 1.4 MyClass.java
Beim Aufruf des Interpretierers, der Java Virtual Machine, ist anzugeben, ob die Zusicherungen zur Laufzeit geprüft werden soll:
java -enableassertions MyClass
java -ea MyClass
oder nicht:
java -disableassertions MyClass
java -da MyClass
Die Standardeinstellung ist, dass die Zusicherungen nicht geprüft werden. Eine verletzte Zusicherung führt zu einem Abbruch des Programmablaufs mit einer Fehlermeldung der Art:
Exception in thread "main" java.lang.AssertionError: Zeichenkette
at MyClass.main(MyClass.java:5)
C#
C# zu ergänzen.
1
http://java.sun.com/developer/technicalArticles/JavaLP/assertions/ (Zugriff 2008-11-24),
http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html (Zugriff 2008-11-24).
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
Anweisungen
8.3.1
Übersicht
8 – 14
Tabelle 8.6 Anweisungen
Component Pascal
Eiffel
C++
Java
C#
Bezeichner = Ausdruck
Bezeichner := Ausdruck
Prozedurbezeichner (Aktualparameterliste)
Blockanweisung: {...}
IF ... THEN ...
ELSIF ... THEN ...
ELSE ... END
if ... then ...
elseif ... then . . .
else ... end
CASE ... OF
| ... : ...
| ... : ...
ELSE ... END
inspect ...
when ... then ...
when ... then ...
else ... end
WITH ... DO ...
| ... DO ...
ELSE ... END
WHILE ... DO ... END
REPEAT ... UNTIL ...
FOR ... TO ... BY ... DO ... END
LOOP ... END
EXIT
RETURN ...
-
if (...) ...
else ...
switch (...)
case . . . : . . . break;
case . . . : . . . break;
default : ...
Siehe 7.12 S. 7-10
while (...) ...
from ...
invariant ...
variant ...
until ...
loop ...
end
do ... while (...);
for (. . .; . . .; . . .) ...
-
break;
return ...;
27.9.12
8 Ablaufsteuerung und algorithmische Konzepte
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
8.3
8.3 Anweisungen
8.3.2
8 – 15
Syntaktischer Zucker
Ein Programmkonstrukt weist eine Unstetigkeitsstelle auf, wenn zwei Programme, die
dieses Konstrukt verwenden und sich in nur einem Zeichen unterscheiden, verschiedene Semantik haben.
Component Pascal,
Eiffel
Alle Steuerkonstrukte benutzen Wortsymbole, um Anweisungsfolgen zu klammern.
Deshalb ist keine Verbundanweisung erforderlich.1 Das schließende END bzw. end
beseitigt Unstetigkeitsstellen und Zweideutigkeiten älterer Sprachen.
In Component Pascal ist das Semikolon ; Endezeichen bei Vereinbarungen und Trennzeichen zwischen Anweisungen.
In Eiffel ist das Semikolon bei Vereinbarungen und Anweisungen ein optionales Trennzeichen, d.h. es kann auch weggelassen werden, weil die Syntax so definiert ist, dass es
auf ein Semikolon mehr oder weniger nicht ankommt. Der empfohlene Stil ist jedoch,
trennende Semikola zwecks besserer Lesbarkeit hinzuschreiben (?).
C++
Steuerkonstrukte erwarten immer eine einzelne Anweisung und enden meist ohne
Wortsymbol. Deshalb ist eine Verbundanweisung erforderlich, sie verwendet die
geschweiften Klammern:
{ Anweisungen }
Manche Konstrukte haben Unstetigkeitsstellen. Beispiel:
while (i < 9) ; i++;
while (i < 9) i++;
Manche Konstrukte liefern Zweideutigkeiten, die durch zusätzliche Sprachregeln aufgelöst werden müssen. Beispiel:
if (i < 9) if (k > 1) i = k--; else i = k++;
Das Semikolon ist bei manchen Anweisungen ein Endezeichen (Deklaration, Ausdruck, do, break, return), bei anderen nicht. Ein defensiver Schreibstil ist, das Semikolon als Endezeichen bei jeder Anweisung zu setzen.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
8.3.3
Semantisches Salz
Solches soll man bekanntermaßen nicht in offene Wunden schütten.
Component Pascal,
Eiffel
Vereinbarungen (declaration), Anweisungen (statement, instruction) und Ausdrücke
(expression) sind verschiedene syntaktische Einheiten. Zuweisungen sind spezielle
Anweisungen. Ausdrücke treten nur als Bestandteile von Anweisungen auf.
Bedingungen in Steuerkonstrukten sind stets boolesche Ausdrücke.
C++
Eine Vereinbarung (declaration) ist eine spezielle Anweisung (statement). Ein Ausdruck (expression) ist eine spezielle Anweisung. Eine Zuweisung ist ein spezieller Ausdruck. Ausdrücke treten als Bestandteile von Vereinbarungen und Anweisungen auf.
C und C++ sind ausdrucksorientierte Sprachen, da sie nicht zwischen Ausdruck und
Anweisung unterscheiden. Die Ausführung einer Ausdrucks-Anweisung macht nur
Sinn, wenn der Ausdruck bei seiner Auswertung einen Nebeneffekt bewirkt. Der Wert
des Ausdrucks kann dabei irrelevant sein. Solche Sprachen beruhen also auf dem Konzept Nebeneffekte bewirkender Ausdrücke. In der Tat gibt es in C/C++ viele Operato-
1
27.9.12
Pascal benötigte sie noch in Form von begin ... end.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
8 – 16
8 Ablaufsteuerung und algorithmische Konzepte
ren, die Nebeneffekte bewirken, und oft werden Funktionen mit Nebeneffekten programmiert.1
Eine Bedingung in einem Steuerkonstrukt ist ein beliebiger Ausdruck, der implizit zu
einem booleschen Wert konvertiert wird.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
8.3.4
Zuweisungen
C++
Man beachte, dass C/C++ als
Zuweisungsoperator =
und als
Gleichheitsoperator ==
verwenden.2 Die Ausdrucksorientierung stellt uns eine Falle für Tippfehler, die der
Kompilierer nicht findet, da sowohl a = b als auch a == b Ausdrücke sind. Der Ausdruck
a=b
liefert als Wert den Wert von b, als Nebeneffekt wird dieser Wert der Variablen a zugewiesen. Der Ausdruck
a=b=c
wird von rechts nach links ausgewertet, also wie
a = (b = c)
Java
Java zu ergänzen.
C#
C# zu ergänzen.
8.3.5
Auswahlanweisungen
8.3.5.1
Alternative Bedingungen
C++
Die if-Anweisung hat keinen elsif-Zweig, dieser kann aber durch geschickte Klammerung simuliert werden.
1
Das Konzept ausdrucksorientierter Sprachen wurde erstmals 1966 von Niklaus Wirth vorgeschlagen und in seiner Programmiersprache Euler realisiert. Über Algol 68 fand es Eingang in C
– interessanterweise zu einem Zeitpunkt, als Wirth selbst dieses Konzept bereits verworfen
hatte und in Pascal das Konzept der Trennung von Ausdruck und Anweisung realisierte.
Aus meiner Sicht ist ein problematischer Aspekt von C/C++, dass sie auf Nebeneffekten beruhen, weil dieses Konzept einer systematischen Entwicklung beweisbar korrekter Programme
wie auch der Lesbarkeit und Verständlichkeit von Programmen entgegenwirken kann. Mit
Nebeneffekten vernünftig programmieren ist zwar möglich, fordert aber Einsicht und Disziplin.
2
In der Mathematik wird seit rund 350 Jahren weltweit einheitlich das Zeichen „=“ als Gleichheitszeichen benutzt. Die meisten Programmiersprachen haben „=“ für denselben Zweck übernommen und „:=“ als Zuweisungszeichen eingeführt. Wenn C/C++ dafür „==“ und „=“
verwenden, so ist das zwar einerseits nur ein syntaktischer Unterschied, andererseits aber ein
nicht gerade geniales Abweichen von einem Standard.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
8.3 Anweisungen
8 – 17
if (Ausdruck)
{
Anweisungen
}
else if (Ausdruck)
{
Anweisungen
}
else
{
Anweisungen
};
Java
Java zu ergänzen.
C#
C# zu ergänzen.
8.3.5.2
Alternative Werte
Wir betrachten jeweils dasselbe Beispiel.
Component Pascal
CASE thisMonth OF
| january .. june :
StudyHardAtCollege;
| july, august :
EnjoyHolidays;
| september :
LookForAJob;
ELSE
GoToWork;
END;
Eiffel
inspect this_month
when january .. june then
study_hard_at_college
when july, august then
enjoy_holidays
when september then
look_for_a_job
else
go_to_work
end
C++
switch (this_month) {
case january :
case february :
case march :
case april :
case may :
case june :
study_hard_at_college ();
break;
case july :
case august :
enjoy_holidays ();
break;
case september :
look_for_a_job ();
break;
default :
go_to_work ();
};
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
8 – 18
8 Ablaufsteuerung und algorithmische Konzepte
Ein case dient nur als Einsprungmarke, von da an werden alle folgenden Anweisungen
ausgeführt bis zur ersten break-Anweisung; diese führt zu einem Sprung ans Ende der
switch-Anweisung. Vergessene break-Anweisungen sind eine Fehlerquelle.
Es gibt keine Listen und Intervalle von Fällen, diese können durch mehrere case ohne
abschließendes break simuliert werden.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
8.3.6
Wiederholungsanweisungen
Wir betrachten nur ein Beispiel – die Summierung der ersten positiven geraden Zahlen
bis 10 – in verschiedenen Formen: als kopf- und fußgesteuerte Bedingungsschleife, als
Zählschleife und als bedingungslose Schleife mit einer Sprunganweisung.
Component Pascal
VAR s, i : INTEGER;
(* Vereinbart für alle Schleifenarten *)
s := 0;
i := 2;
WHILE i <= 10 DO
INC (s, i);
INC (i, 2);
END;
s := 0;
i := 2;
REPEAT
INC (s, i);
INC (i, 2);
UNTIL i > 10;
s := 0;
FOR i := 2 TO 10 BY 2 DO
INC (s, i);
END;
s := 0;
i := 2;
LOOP
INC (s, i);
IF i >= 10 THEN
EXIT;
ELSE
INC (i, 2);
END;
END;
Eiffel
s, i : INTEGER
from
s := 0
i := 2
invariant
s >= 0;
i >= 2
variant
10 - i
until
i > 10
loop
s := s + i
i := i + 2
end
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
8.4 Testhilfen
8 – 19
Die Initialisierung der Schleifenvariablen sowie die Zusicherungen für Schleifeninvariante und -variante sind integriert. Die Bedingung ist eine Abbruchbedingung am Kopf
der Schleife.
C++
unsigned int s = 0,
i = 2;
// Vereinbart und initialisiert für alle Schleifenarten
// ausgenommen for
while (i <= 10) {
s += i;
i += 2;
}
do {
s += i;
i += 2;
} while (i <= 10);
for (unsigned int k = 2; k <= 10; k += 2) {
s += k;
}
while (true) {
s += i;
if (i >= 10) {
break;
} else {
i += 2;
};
Man beachte, dass bei der fußgesteuerten do-while-Schleife eine Fortsetzungsbedingung steht!
Die for-Schleife ist keine eigentliche Zählschleife, sondern eine while-Schleife, bei der
(1)
(2)
(3)
eine lokale Laufvariable deklariert und initialisiert,
eine Fortsetzungsbedingung und
eine Rumpf-Ende-Aktion angegeben
werden kann.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
8.4
Testhilfen
Zum Testen kann es nützlich sein, zusätzliche Anweisungen in ein Programm einzufügen. Nach dem Test sollten sie vor einem erneuten Kompilationsvorgang ausgeblendet
werden, damit sie keinen Laufzeitaufwand verursachen. Entfernen oder Auskommentieren von Testanweisungen ist ungeschickt, aufwändig und fehleranfällig.
Component Pascal
Zum Ein- und Ausblenden von Testanweisungen können Falter (folder) verwendet
werden. Sie lassen sich benennen (z.B. als GraphicsTest) und mit einem Befehl selektiv
für spezifizierte Module ein- und ausklappen.
Eiffel
Zum Ein- und Ausblenden von Testanweisungen wird bedingte Kompilation verwendet. Es gibt dazu eine spezielle Anweisung der Form
debug ("Graphics_Test")
Testanweisungen
end
Per Kompiliereroption werden debug-Anweisungen nicht oder selektiv für spezifizierte
Klassen, Cluster und Schlüssel kompiliert.
27.9.12
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
8 – 20
C++
8 Ablaufsteuerung und algorithmische Konzepte
Zum Ein- und Ausblenden von Testanweisungen wird üblicherweise bedingte Kompilation verwendet. Diese Möglichkeit bietet der Vorübersetzer mit Direktiven der Form
#define GRAPHICS_TEST 1
#ifdef GRAPHICS_TEST
Testanweisungen
#endif
Java
Java zu ergänzen.
C#
C# zu ergänzen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
9
Syntaktische und lexikalische Aspekte
Aufgabe 9
9.1
Kommentare
Component Pascal
Kommentare werden mit (* und *) geklammert. Sie können geschachtelt werden.
Eiffel
Es gibt nur Zeilenendkommentare. Sie werden mit -- eingeleitet und erstrecken sich bis
zum Zeilenende.
C++
Kommentare werden mit /* und */ geklammert oder als Zeilenendkommentare mit //
eingeleitet, sie erstrecken sich dann bis zum Zeilenende. Es gibt keine geschachtelten
Kommentare.
Java
Wie bei C++; zusätzlich werden mit /** und */ geklammerte Kommentare vom Werkzeug Javadoc in HTML-Dokumentationsdateien extrahiert.
C#
Wie bei C++; zusätzlich werden mit /// eingeleitete Kommentare von einem .NETWerkzeug in XML-Dokumentationsdateien extrahiert.
9.2
Zeichencodes
Component Pascal,
Java, C#
Die Sprachdefinitionen nehmen Bezug auf den Unicode.
Eiffel
Die Sprachdefinition nimmt Bezug auf den ASCII-Zeichencode.
C++
Die Sprachdefinition bezieht sich auf den ASCII-Zeichencode. Die Sprache ist jedoch
nicht auf diesen festgelegt, da Vorkehrungen getroffen sind, um sie mit anderen Zeichencodes zu realisieren.
9.3
Bezeichner
Component Pascal
Frei wählbare Bezeichner bestehen aus Latin1-alphanumerischen Zeichen und dem
Unterstrich. Das erste Zeichen ist nicht numerisch. Klein- und Großschreibung wird
unterschieden. Der Unterstrich dient gemäß Konvention nur der Kompatibilität mit
anderen Sprachen; üblich ist die von Smalltalk stammende Groß-Kleinschreibung.
Jedes Modul hat seinen eigenen Namensraum. Ein Bezeichner kann in einem Modul
mehrfach für verschiedene Größen verwendet werden. Namenskonflikte werden durch
Sichtbarkeitsregeln gelöst.
Eiffel
Frei wählbare Bezeichner bestehen aus alphanumerischen Zeichen und dem Unterstrich. Das erste Zeichen ist alphabetisch. Klein- und Großschreibung wird nicht unterschieden.
Es gibt getrennte Namensräume für Klassen und Größen (Merkmale, Parameter usw.).
Ein Bezeichner kann innerhalb einer Klasse nur für eine Größe verwendet werden.
Klassennamen werden gemäß verbreiteter Konvention großgeschrieben, andere
Bezeichner klein.
C++
Frei wählbare Bezeichner bestehen aus alphanumerischen Zeichen und dem Unterstrich. Das erste Zeichen ist nicht numerisch. Klein- und Großschreibung wird unterschieden.
Ein Bezeichner kann in einem Programm mehrfach für verschiedene Größen verwendet werden. Namenskonflikte werden durch Sichtbarkeitsregeln gelöst.
Java
Java zu ergänzen.
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Kapitel 9 – Seite 1 von 2
9–2
9 Syntaktische und lexikalische Aspekte
C#
C# zu ergänzen.
9.4
Wortsymbole
Wortsymbole sind von der Sprache reservierte Schlüsselwörter.
Component Pascal
Wortsymbole und sprachdefinierte Namen werden generell großgeschrieben.
Eiffel
Bei Wortsymbolen wird nicht zwischen Klein- und Großschreibung unterschieden. Die
verbreitete Konvention ist Kleinschreibung.
C++
Wortsymbole werden kleingeschrieben. Durch Vorübersetzerdirektiven können ihnen
zusätzliche Bezeichner zugeordnet werden.
Java
Java zu ergänzen.
C#
C# zu ergänzen.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
A
Vorbemerkung
Literaturverzeichnis
Publikationen zu Informatik- und Softwarethemen erscheinen zahlreich – sowohl
Bücher und Zeitschriften als auch Beiträge im Web. Seriöse Verlage wie die hier vertretenen und andere bieten wissenschaftlich, technisch und didaktisch fundierte Werke
kompetenter Autoren an.
Andererseits hat eine an grellbunten Umschlägen erkennbare Regenbogenpresse der
I&KT einen großen Marktanteil erorbert. Kritikpunkte daran sind: Autoren sind oft
Nichtfachleute, z.B. Journalisten, die peinliches Halbwissen reproduzieren. Titel orientieren sich vor allem an gängigen Produkten, oft sind es schwache Übersetzungen oder
Plagiate von Originalhandbüchern. Die Inhalte sind oft fehlerhaft, schluderig ungenau,
unsystematisch, geschwätzig, unnötig redundant. Bei Programmiersprachen werden oft
herstellerspezifische Produktdetails als „Standard“ angepriesen. Mit „Tipps und
Tricks“ statt wissenschaftlich-ingenieurmäßiger Methodik wendet sich diese Presse
eher an Laien und Hacker als an Experten. Zudem ist das Preis/Qualität-Verhältnis
fragwürdig. Deshalb stehen solche Publikationen nicht in diesem Literaturverzeichnis.
Verlage
Grundlagen und
Algorithmik
Addison-Wesley
Addison-Wesley, Boston/San Francisco/New York/Toronto u.a.
Addison Wesley Longman Inc., Reading, Massachusetts
Addison-Wesley Publishing Company, Reading, Massachusetts
Addison-Wesley Pearson Education Inc., Upper Saddle River, NJ
Addison-Wesley (Deutschland) GmbH, Bonn
Addison-Wesley Verlag, München
dpunkt
dpunkt.verlag GmbH, Heidelberg
Hanser
Carl Hanser Verlag, München/Wien
Oldenbourg
R. Oldenbourg Verlag GmbH, München/Wien
Oldenbourg Wissenschaftsverlag GmbH, München
Prentice Hall
Prentice Hall, Englewood Cliffs
Prentice Hall, Upper Saddle River, New Jersey
Prentice Hall, München
Spektrum
Spektrum Akademischer Verlag GmbH, Heidelberg/Berlin
Springer
Springer-Verlag, Barcelona/Berlin/Budapest/Dordrecht/Heidelberg/
Hong Kong/London/New York/Paris/Tokyo/Wien u.a.
Vieweg
Friedr. Vieweg & Sohn Verlagsgesellschaft mbH, Braunschweig/
Wiesbaden
Friedr. Vieweg & Sohn Verlag / GWV Fachverlage GmbH, Wiesbaden
[ApL92]
Hans-Jürgen Appelrath, Jochen Ludewig: Skriptum Informatik – eine konventionelle Einführung. Verlag der Fachvereine, Zürich; Teubner, Stuttgart
(1992) 2. Aufl., 448 S.
[GoZ06]
Gerhard Goos, Wolf Zimmermann: Vorlesungen über Informatik. Band 2:
Objektorientiertes Programmieren und Algorithmen. Springer (2006)
4. überarbeit. Aufl., 375 S.
[HuA04]
Peter Hubwieser, Gerd Aiglstorfer: Fundamente der Informatik. Ablaufmodellierung, Algorithmen und Datenstrukturen. Oldenbourg (2004)
276 S.
[Lan03]
Hans Werner Lang: Algorithmen in Java. Oldenbourg (2003) 261 S.
© K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012
Anhang A – Seite 1 von 10
A–2
A Literaturverzeichnis
[Mey09]
Bertrand Meyer: Touch of Class. Learning to Program Well with Objects
and Contracts. Springer (2009) 876 S.
Empfehlenswertes Werk über Grundlagen der objektorientierten Programmierung, richtet sich an Informatikstudenten im Grundstudium.
[ScW01]
Uwe Schneider, Dieter Werner (Hrsg.): Taschenbuch der Informatik.
Fachbuchverlag Leipzig im Carl Hanser Verlag (2001) 4. aktual. Aufl.,
876 S.
Überblick über 25 Gebiete der Informatik, darunter 52 Seiten über Programmiersprachen (leider mit Ungenauigkeiten und fehlerhaften Beispielalgorithmen).
Software-Entwurf und
-Entwicklung
[GHJV94] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Addison-Wesley (1994) 416 S.
Die „Viererbande“ präsentiert hier einen Katalog einfacher und präziser Lösungen für wiederkehrende Entwurfsprobleme der objektorientierten Softwareentwicklung.
[GHJV04] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software. Addison-Wesley (2004) 504 S.
Deutsche Ausgabe von [GHJV94].
[JTM00]
Jean-Marc Jézéquel, Michel Train, Christine Mingins: Design Patterns
and Contracts. Addison-Wesley (2000) 348 S.
Präsentiert die Entwurfsmuster aus [GHJV94] mit vertraglichen Spezifikationen in Eiffel.
[Kuc04]
Partha Kuchana: Software Architecture Design Patterns in Java. Auerbach
Publications, Boca Raton, Florida (2004) 492 S.
Präsentiert die Entwurfsmuster aus [GHJV94] und weitere in Java.
[LaR06]
Bernhard Lahres, Gregor Raýman: Praxisbuch Objektorientierung. Von
den Grundlagen zur Umsetzung. Galileo Press, Bonn (2006) 609 S.
Spannt den Bogen von Prinzipien der Softwareentwicklung zu Beispielen in C++, Java, C# und JavaScript.
[Mey90]
Bertrand Meyer: Objektorientierte Softwareentwicklung. Hanser (1990)
547 S.
Ein Standardwerk und Bestseller der Objektorientierung; liefert eine
softwaretechnisch begründete Einführung in objektorientierte Konzepte und die Programmiersprache Eiffel.
[Mey97]
Bertrand Meyer: Object-oriented Software Construction. Prentice Hall
(1997) 2nd edition, 1254 S.
Sehr empfehlenswerte, neue, überarbeitete Auflage der Originalausgabe von [Mey90].
[Oes04]
Bernd Oestereich: Objektorientierte Softwareentwicklung. Analyse und
Design mit der UML 2.0. Oldenbourg (2004) 6. völlig überarbeit. Aufl.,
377 S.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
A–3
A Literaturverzeichnis
[ReP99]
Peter Rechenberg, Gustav Pomberger (Hrsg.): Informatik-Handbuch.
Hanser (1999) 2. aktual. u. erweit. Aufl., 1166 S.
Empfehlenswertes Nachschlagewerk mit Beiträgen von 47 Experten zu
Themen aus theoretischer, technischer, praktischer, angewandter und
Wirtschaftsinformatik sowie zu Daten, Normen und Spezifikationen.
Programmiersprachen
[BaG94]
Henri Bal, Dick Grune: Programming Language Essentials. AddisonWesley (1994) 288 S.
[BeG96]
Thomas J. Bergin Jr., Richard G. Gibson Jr. (eds.): History of Programming Languages II. ACM Press, New York, Addison-Wesley (1996) 864
S.
Konferenzbeiträge zu Algol 68, Pascal, Concurrent Pascal, Ada, Lisp,
Prolog, Simulationssprachen, Formac, CLU, Smalltalk, Icon, Forth, C
und C++.
[BGP00]
László Böszörményi, Jürg Gutknecht, Gustav Pomberger (eds): The
School of Niklaus Wirth. „The Art of Simplicity“. dpunkt; Morgan Kaufmann Publishers, San Francisco (2000) 260 S.
[GoZ99]
Gerhard Goos, Wolf Zimmermann: Programmiersprachen. In: [ReP99]
S. 469–515
[Gru02]
Dominik Gruntz: C# and Java: The Smart Distinctions. In: Journal of
Object Technology, Vol. 1, No. 5 (November–December 2002) S. 163–
176, http://www.jot.fm/issues/issue_2002_11/article4 (Zugriff 2010-0304)
[HeV07h]
Peter A. Henning, Holger Vogelsang (Hrsg.): Handbuch Programmiersprachen. Softwareentwicklung zum Lernen und Nachschlagen. Hanser
(2007) 786 S.
23 aktuelle Programmiersprachen in einzelnen Übersichtsbeiträgen.
[HeV07t]
Peter A. Henning, Holger Vogelsang (Hrsg.): Taschenbuch Programmiersprachen. Fachbuchverlag Leipzig im Carl Hanser Verlag (2007) 2. neu
bearbeit. Aufl. 631 S.
Kleiner Bruder von [HeV07h], 22 aktuelle Programmiersprachen in
einzelnen Übersichtsbeiträgen.
[Joy99]
Ian Joyner: Objects Unencapsulated. Java, Eiffel, and C++?? Prentice
Hall (1999) 386 S.
[Kur01]
Budi Kurniawan: Comparing C# and Java. O'Reilly Media, Inc. (2001)
13 S.,
http://ondotnet.com/pub/a/dotnet/2001/06/07/csharp_java.html
(Zugriff 2010-03-04)
[Lin96]
Charles H. Lindsey: A History of Algol 68. In: [BeG96] S. 27–96
[Lou93]
Kenneth C. Louden: Programming Languages. Principles and Practice.
PWS Publishing Company, Boston (1993) 592 S.
Behandelt Geschichtliches, Entwurfsprinzipien, Syntax, Semantik,
Datentypen, Ablaufsteuerung, objektorientiertes, funktionales, logisches und nebenläufiges Programmieren und formale Semantikbeschreibungen mit Beispielen in Fortran, Pascal, C, Ada, Modula-2,
Smalltalk, C++, Eiffel, ML, Scheme und Prolog. Eine deutsche Ausgabe ist unter dem Titel „Programmiersprachen“ erschienen.
[PrZ98]
27.9.12
Terrence Pratt, Marvin Zelkowitz: Programmiersprachen. Design und
Implementierung. Prentice Hall (1998) 816 S.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
A–4
A Literaturverzeichnis
[Sal98]
Peter H. Salus (ed.): Handbook of Programming Languages, Volume I.
Object-Oriented Programming Languages. Macmillan Technical Publishing, USA (1998) 944 S.
Enthält Beiträge der Sprachentwerfer zu Smalltalk, C++, Eiffel,
Ada 95, Modula-3 und Java.
[Seb06]
Robert W. Sebesta: Concepts of Programming Languages. Pearson Education Inc., Boston (2006) 7th edition, 724 S.
Behandelt Geschichtliches, Syntax, Semantik, Namen, Datentypen,
Ausdrücke, Ablaufsteuerung, Unterprogramme, objektorientiertes Programmieren, Nebenläufigkeit, Ausnahmebehandlung und funktionale
und logische Programmiersprachen mit Beispielen in Fortran, Ada,
Smalltalk, C++, Java, C#, JavaScript, PHP, Lisp, ML, Haskell und
Prolog. Interessant sind die eingestreuten historischen Anmerkungen
und Interviews mit Sprachentwerfern.
[Set97]
Ravi Sethi: Programming Languages. Concepts and Constructs. AddisonWesley (1997) 2nd edition, 640 S.
Behandelt Syntax, Anweisungen, Datentypen, Prozeduren, objektorientiertes, funktionales, logisches und nebenläufiges Programmieren,
formale Semantikbeschreibungen und den Lambdakalkül mit Beispielen in Pascal, C, Smalltalk, C++, ML, Scheme, Prolog und Ada.
[Sta95]
Ryan Stansifer: The Study of Programming Languages. Prentice Hall
(1995) 334 S.
Eher theoretisch orientiert mit Beispielen aus vielen Sprachen;
Geschichtliches, Syntax, Grammatiken, Datentypen, Blöcke, Prozeduren, Module, Prolog, Lambdakalkül, denotationale und axiomatische
Semantik.
[Wir05]
Niklaus Wirth: Good Ideas, Through the Looking Glass. Zürich (2005)
28 S., http://www.cs.inf.ethz.ch/~wirth/Articles/GoodIdeas_origFig.pdf
(Zugriff 2010-02-18)
[Woo96]
Mark Woodman (ed.): Programming Language Choice. Practice and
Experience. International Thomson Computer Press, London/Boston
(1996) 384 S.
[Zep04]
Klaus Zeppenfeld: Objektorientierte Programmiersprachen. Einführung
und Vergleich von Java, C++, C# und Ruby. Spektrum (2004) 362 S.
Stellt die vier Sprachen nacheinander vor, jeweils mit Details beginnend; spricht Konzepte und Sprachvergleiche nur knapp an.
Component Pascal
[CPLR06] Oberon microsystems, Inc.: Component Pascal Language Report. (October 2006) 32 S., http://www.oberon.ch/pdf/CP-Lang.pdf (Zugriff 201002-18)
[Fra00]
Michael Franz: Oberon – The Overlooked Jewel. In: [BGP00] S. 41–53
[Hug01]
Karlheinz Hug: Module, Klassen, Verträge. Ein Lehrbuch zur komponentenorientierten Softwarekonstruktion. Vieweg (2001) 2. Aufl., 446 S.
[Mös94]
Hanspeter Mössenböck: Objektorientierte Programmierung in Oberon-2.
Springer (1994) 2. Aufl., 286 S.
Eine Einführung in objektorientiertes Programmieren; setzt Programmierkenntnisse voraus, enthält Oberon-2-Referenzmanual.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
A–5
A Literaturverzeichnis
[ReW94]
Martin Reiser, Niklaus Wirth: Programmieren in Oberon. Das neue Pascal. Addison-Wesley (1994) 338 S.
Ein einführendes Programmierlehrbuch für Oberon mit einem Kapitel
über Oberon-2; gut für Anfänger geeignet.
[War02]
Eiffel
J. Stanley Warford: Computing Fundamentals. The Theory and Practice of
Software Design with BlackBox Component Builder. Vieweg (2002) 611
S.
[ECMA367] Standard ECMA-367: Eiffel Analysis, Design and Programming Language. (June 2006) 2nd Edition, 174 S., http://www.ecma-international.org/
publications/files/ECMA-ST/ECMA-367.pdf (Zugriff 2012-03-14)
Verabschiedete Version des Eiffel-ECMA-Standards der European
Computer Manufacturers Association, beruht auf [Mey92].
[ER06]
Eiffel: The Reference (Working draft). (1985–2006) http://archive.eiffel.com/nice/language/ (Zugriff 2006-04-07)
Die weiter entwickelte Sprachreferenz [Mey92], die dem NICE-Standardisierungskomitee als Vorlage dient, hier als aus FrameMaker generierte HTML-Variante.
[Gor96]
Jacob Gore: Object Structures. Building Object-Oriented Software Components with Eiffel. Addison-Wesley (1996) 469 S.
[Jez96]
Jean-Marc Jézéquel: Object-Oriented Software Engineering with Eiffel.
Addison Wesley (1996) 340 S.
[MaS01]
Glenn Maughan, Raphael Simon: Windows Programming Made Easy.
Using Object Technology, COM, and the Windows Eiffel Library. Prentice
Hall (2001) 726 S.
GUI-Programmierung auf Microsoft Windows mit Eiffel.
[Mey92]
Bertrand Meyer: Eiffel: The Language. Prentice Hall (1992) 594 S.
Der Entwerfer von Eiffel diskutiert in diesem umfassenden, kommentierten Referenzmanual auch Ziele, Konzepte und Entwurfsentscheidungen.
[Mey95]
Bertrand Meyer: Eiffel: The Reference. ISE Technical Report TR-EI-41/
ER (1995) Version 3.3.4, 100 S.
Kurzfassung von [Mey92].
27.9.12
[Mey98]
Bertrand Meyer: Eiffel. In: [Sal98] S. 461–551
[Mon93]
Frieder Monninger: Eiffel. Objektorientiertes Programmieren in der Praxis. H. Heise Verlag, Hannover (1993) 268 S.
[Swi93]
Robert Switzer: Eiffel: An Introduction. Prentice Hall (1993) 161 S.
[ThW95]
Pete Thomas, Ray Weedon: Object-Oriented Programming in Eiffel.
Addison-Wesley (1995) 518 S.
[TrC01]
Jean-Paul Tremblay, Grant A. Cheston: Data Structures and Software
Development in an Object-Oriented Domain. Eiffel Edition. Prentice Hall
(2001) 1039 S.
[Wie95]
Richard Wiener: Software Development Using Eiffel. There Can Be Life
Other Than C++. Prentice Hall (1995) 425 S.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
A–6
C
A Literaturverzeichnis
[DaM91]
Peter A. Darnell, Philip E. Margolis: C: A Software Engineering
Approach. Springer (1991) 2nd edition, 622 S.
Meiner Ansicht nach unter den vielen ein gutes C-Buch.
[KeR88]
Brian W. Kernighan, Dennis M. Ritchie: The C Programming Language,
Second Edition, ANSI C. Prentice Hall (1988)
Das Standardwerk der Entwerfer von C.
[KeR90]
Brian W. Kernighan, Dennis M. Ritchie: Programmieren in C. Mit dem CReference-Manual in deutscher Sprache. Hanser (1990) 2. Ausgabe,
ANSI C, 283 S.
Deutsche Ausgabe von [KeR88].
C++
Über C++ gibt es hunderte von Büchern, viele von der Bauart „Von C nach C++“ mit
dem Nachteil, dass sie einem prozeduralen C-Programmierstil verhaftet bleiben und
objektorientierte Konzepte und moderne Programmierstile vernachlässigen. Das Folgende beschränkt sich auf einige solide Standardwerke von Autoren mit profunden
C++-Kenntnissen, die meist an der Entwicklung der Sprache selbst beteiligt waren
oder sind.
[BaN94]
John J. Barton, Lee R. Nackman: Scientific and Engineering C++: An
Introduction with Advanced Techniques and Examples. Addison-Wesley
(1994)
Für Techniker mit Problemen, deren Lösungen früher mit Fortran programmiert wurden.
[Cop92]
James O. Coplien: Advanced C++ Programming Styles and Idioms. Addison-Wesley (1992) reprinted with corrections 1994, 520 S.
Werk eines C++-Experten für C++-Erfahrene; beginnt, wo [FrK94]
endet, mit Datenabstraktion und objektorientierter Programmierung.
[C++98]
ISO/IEC 14882: International Standard, Programming Languages –
C++, Langages de programmation – C++. American National Standards
Institute, New York (1998-09-01) First edition, 776 S., http://wwwd0.fnal.gov/~dladams/cxx_standard.pdf (Zugriff 2012-03-06)
C++-Standard der International Standards Organization, umfasst auch
die Standardbibliothek.
[C++11]
ISO/IEC 14882: Working Draft, Standard for Programming Language
C++. Document Number: N3242=11-0012 (2011-02-28) 1332 S., http://
www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf (Zugriff
2012-03-06)
Freie Vorversion des künftigen C++-Standards.
[ElS90]
Margaret A. Ellis, Bjarne Stroustrup: The Annotated C++ Reference
Manual. Addison-Wesley (1990) reprinted with corrections 1995, 470 S.
Vollständiges, alle Sprachkonstrukte mit Beispielen kommentierendes
Referenzmanual, Basisdokument des ANSI/ISO-C++-Standards,
exakteste (nicht mehr aktuelle) Quelle für Experten, als Lehrbuch für
Anfänger ungeeignet.
[FrK94]
Frank L. Friedman, Elliot B. Koffman: Problem Solving, Abstraction, and
Design Using C++. Addison-Wesley (1994) 888+ S.
Umfangreiche Einführung, dringt aber nur bis zu abstrakten Datentypen vor. Objektorientierte Konzepte, Generizität, Ausnahmebehandlung und andere neue Sprachkonzepte werden nicht behandelt.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
A–7
A Literaturverzeichnis
[Jos96]
Nicolai Josuttis: Die C++-Standardbibliothek. Eine detaillierte Einführung in die vollständige ANSI/ISO-Schnittstelle. Addison-Wesley (1996)
570 S.
[Koe98]
Andrew Koenig: C++ Traps and Pitfalls. In: [Sal98] S. 405–458
[KoM00]
Andrew Koenig, Barbara E. Moo: Accelerated C++. Practical Programming by Example. Addison-Wesley (2000) 336 S.
Beruht auf einem einwöchigen Intensivkurs zu C++, der mit dem
Benutzen von Standardbibliotheksklassen startet und dann die Sprachkonzepte behandelt, mit denen die Standardbibliothek implementiert
ist.
[KoM03]
Andrew Koenig, Barbara E. Moo: Intensivkurs C++. Schneller Einstieg
über die Standardbibliothek. Pearson Studium, München (2003) 427 S.
Deutsche Ausgabe von [KoM00].
[Lip91a]
Stanley B. Lippman: C++ Primer. Addison-Wesley (1991) 2nd edition,
reprinted with corrections 1995, 614 S.
Eine der raren brauchbaren Einführungen eines intimen Kenners von
C++; setzt Programmierkenntnisse voraus; beginnt aber erst nach 200
Seiten mit objektorientierter Programmierung; nicht auf Stand des
C++-Standards.
[Lip91b]
Stanley B. Lippman: C++ Einführung und Leitfaden. Addison-Wesley
(1991) 2. erweit. Aufl., korrig. Nachdruck 1994, 622 S.
Deutsche Ausgabe von [Lip91a].
[Str91]
Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley
(1991) 2nd edition, reprinted with corrections 1995, 691 S.
[Str94a]
Bjarne Stroustrup: The Design and Evolution of C++. Addison-Wesley
(1994) 461 S.
Der Meister schildert, wie und warum sein Werk das wurde, was es ist.
Die Diskussion der Ziele, Konzepte und Entwurfsentscheidungen liefert viele Einsichten, die zu besserem Verständnis von C++ beitragen –
setzt aber C++-Kenntnisse voraus.
[Str94b]
Bjarne Stroustrup: Die C++ Programmiersprache. Addison-Wesley
(1994) 2. Aufl., 4. korrig. u. erweit. Nachdruck, 717 S.
Deutsche Ausgabe der 2. Auflage von [Str97].
[Str94c]
Bjarne Stroustrup: Design und Entwicklung von C++. Addison-Wesley
(1994) 576S
Deutsche Ausgabe von [Str94a].
[Str97]
Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley
(1997) 3rd edition, 911 S., http://www.ib.cnea.gov.ar/~oop/biblio/
Bjarne_Stroustrup_-_The_C++_Programming_Language_3rd_Ed.pdf
(Zugriff 2012-03-06)
Das Standardwerk über C++, geschrieben von seinem Entwerfer und
ersten Implementierer, setzt Programmierkenntnisse voraus, deckt alle
ANSI/ISO-Standard-Sprachkonzepte und die Standardbibliothek ab,
enthält ein Referenzmanual.
[Str98a]
27.9.12
Bjarne Stroustrup: A History of C++. In: [Sal98] S. 196–303
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
A–8
A Literaturverzeichnis
[Str98b]
Bjarne Stroustrup: A Detailed Introduction to C++. In: [Sal98] S. 305–
403
[Str00]
Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley
(2000) special edition, 1029 S.
Gebundene Ausgabe von [Str97] mit zwei zusätzlichen kleinen Anhängen und 1000 kleinen Verbesserungen.
Java
Auch zu Java gibt es eine umfangreiche Literatur, aus der nur einige Titel genannt sind.
[Abt04]
Dietmar Abts: Grundkurs JAVA. Von den Grundlagen bis zu Datenbankund Netzanwendungen. Vieweg (2004) 4. verbess. u. erweit. Aufl., 408 S.
[AGH05]
Ken Arnold, James Gosling, David Holmes: The JavaTM Programming
Language, Fourth Edition. Prentice Hall PTR (2005) 928 S.
[ArG96]
Ken Arnold, James Gosling: The JavaTM Programming Language. Addison-Wesley (1996)
[BlN05]
Joshua Bloch, Neal Gafter: JavaTM Puzzlers. Traps, Pitfalls, and Corner
Cases. Addison-Wesley (2005) 282 S.
[Blo01]
Joshua Bloch: Effective JavaTM Programming Language Guide. AddisonWesley (2001) 252 S.
[Fla97]
David Flanagan: Java in a Nutshell: A Desktop Quick Reference. O’Reilly,
Sebastopol (1997)
[GJS96]
James Gosling, Bill Joy, Guy Steele: The JavaTM Language Specification.
Addison-Wesley (1996) 825 S.
[GJSB00]
James Gosling, Bill Joy, Guy Steele, Gilad Bracha: The JavaTM Language
Specification, Second Edition. Prentice Hall PTR (2000) 544 S.; Sun Microsystems, Inc.: http://java.sun.com/docs/books/jls/second_edition/html/
j.title.doc.html (Zugriff 2008-11-24)
[GJSB05]
James Gosling, Bill Joy, Guy Steele, Gilad Bracha: The JavaTM Language
Specification, Third Edition. Addison Wesley (2005) 688 S.; Oracle: http:/
/docs.oracle.com/javase/specs/jls/se5.0/jls3.pdf, http://docs.oracle.com/
javase/specs/jls/se5.0/html/j3TOC.html (Zugriff 2012-03-06)
[GJSBB11] James Gosling, Bill Joy, Guy Steele, Gilad Bracha, Alex Buckley: The
JavaTM Language Specification, Java SE 7 Edition. (July 2011) 670 S.;
Oracle: http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf,
http://docs.oracle.com/javase/specs/jls/se7/html/index.html
(Zugriff 2012-03-06)
[Gru05]
Ulrich Grude: Java ist eine Sprache. Java lesen, schreiben und ausführen
– Eine präzise und verständliche Einführung. Vieweg (2005) 603 S.
[HeM05]
Steffen Heinzl, Markus Mathes: Middleware in Java. Leitfaden zum Entwurf verteilter Anwendungen – Implementierung von verteilten Systemen
über JMS – Verteilte Objekte über RMI und CORBA. Vieweg (2005)
280 S.
[KüW05]
Wolfgang Küchlin, Andreas Weber: Einführung in die Informatik. Objektorientiert mit Java. Springer (2005) 3. Aufl., 471 S.
Grundkonzepte, Sprachkonzepte, Algorithmen, Theorie.
[Mös01]
Hanspeter Mössenböck: Sprechen Sie Java? Eine Einführung in das systematische Programmieren. dpunkt (2001) 289 S.
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12
A–9
A Literaturverzeichnis
C#
[Now05]
Johannes Nowak: Fortgeschrittene Programmierung mit Java 5. Generics, Annotations, Concurrency und Reflection – mit allen wesentlichen
Neuerungen der J2SE 5.0. dpunkt (2005) 266 S.
[Pep05]
Peter Pepper: Programmieren mit Java. Eine grundlegende Einführung
für Informatiker und Ingenieure. Springer (2005) 488 S.
Von der auf einige Meter angewachsenen C#-Literatur folgen auch nur wenige Titel.
[Arc01]
Tom Archer: Inside C#. Objektorientiertes Programmiern der nächsten
Generation mit C#. Microsoft Press Deutschland, Unterschleißheim
(2001) 351 S.
[ECMA334] ECMA International: Standard ECMA-334 C# Language Specification.
(June 2006) 4th Edition, 553 S., http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf, Hyperlinked: http://www.jaggersoft.com/csharp_standard/ (Zugriff 2012-03-06)
[C#PR12]
C# Programmer’s Reference. Visual Studio .NET 2003. (2012)
http://msdn.microsoft.com/en-us/library/618ayhy6(v=vs.71).aspx
(Zugriff 2012-03-093)
[HWG04]
Anders Hejlsberg, Scott Wiltamuth, Peter Golde: Die C# Programmiersprache. Die komplette Referenz. Addison-Wesley (2004) 692 S.
Deutsche Ausgabe von The C# Programming Language.
Weitere
Programmiersprachen
[Rot04]
Heinrich Rottmann: Warum ausgerechnet .NET? Fakten und Vergleiche
mit Java und C++ – Beispielprogramme – Glasklare Entscheidungshilfen.
Vieweg (2004) 306 S.
[WiH04]
Scott Wiltamuth, Anders Hejlsberg: C# Language Specification. http://
msdn.microsoft.com/library/default.asp?url=/library/en-us/csspec/html/
CSharpSpecStart.asp (Zugriff 2004-09-23)
[D09]
Digital Mars: D Programming Language. http://www.digitalmars.com/d
(Zugriff 2010-03-05)
[WiW66a] Niklaus Wirth, Helmut Weber: EULER: A Generalization of ALGOL, and
its Formal Definition: Part I. Comm. ACM, Vol. 9, No. 1 (January 1966)
S. 13–25, http://dl.acm.org/citation.cfm?id=365162 (Zugriff 2011-10-06)
[WiW66b] Niklaus Wirth, Helmut Weber: EULER: A Generalization of ALGOL, and
its Formal Definition: Part II. Comm. ACM, Vol. 9, No. 2 (February
1966) S. 89–99, http://dl.acm.org/citation.cfm?id=365202 (Zugriff 201110-06)
Zeitschriften
[Go10]
The Go Programming Language. http://golang.org (Zugriff 2010-03-04)
[Sca10]
École Polytechnique Fédérale de Lausanne (EPFL): Scala. Lausanne,
Switzerland (2010) http://www.scala-lang.org (Zugriff 2010-03-04)
[Spe12]
Microsoft Research: Spec#. Microsoft Corporation (2012) http://
research.microsoft.com/en-us/projects/specsharp/ (Zugriff 2012-03-07)
Journal of Object-Oriented Programming. Monatlich
JavaSPEKTRUM. SIGS-DATACOM GmbH, Troisdorf, zweimonatlich
OBJEKTspektrum. SIGS-DATACOM GmbH, Troisdorf, zweimonatlich
The C++ Report.
The C++ Journal. Vierteljährlich
Newsgroups und
Bulletin Boards
27.9.12
BIX:
c.plus.plus
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
A – 10
A Literaturverzeichnis
usenet:
Elektronische
Quellen
comp.lang.c++
Mit Suchmaschinen wie www.google.de findet man spielend zahlreiche Einführungen
in Programmiersprachen.
[Kin]
Bill Kinnersley: The Language List. Collected Information On About 2500
Computer Languages, Past and Present. http://people.ku.edu/~nkinners/
LangList/Extras/langlist.htm (Zugriff 2010-02-18)
[Neu99]
Michael Neumann: „Hello World“ in 65 verschiedenen Sprachen. Letzte
Änderung: 20.11.1999, http://www.ntecs.de/old-hp/s-direktnet/sprachen.htm (Zugriff 2008-06-06)
[Neu03]
Michael Neumann: 433 Beispiele in 132 (oder 162*) Programmiersprachen. Letzte Änderung: 30.11.2003, http://www.ntecs.de/old-hp/uu9r/
lang/html/lang.de.html (Zugriff 2008-06-06)
© Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1
27.9.12