262
Objectorientierte Programmierung mit ANSI-C Axel T. Schreiner

Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

  • Upload
    others

  • View
    3

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

Objectorientierte Programmierung mit ANSI-C

Axel T. Schreiner

Page 2: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

© � � Carl Hanser Verlag München WienAll rights reserved. ISBN 978-1-105-10568-5

Page 3: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

v___________________________________________________________________________

Vorwort

Keine Programmiertechnik löst alle Probleme.Keine Programmiersprache liefert nur korrekte Resultate.

Kein Programmierer sollte bei jedem Projekt ganz neu anfangen.

Objekt-orientierte Programmierung ist zur Zeit ein Allheilmittel — obwohl es dieseMethodik seit viel mehr als zehn Jahren gibt. Im Kern geht es um wenig mehr alsdarum, endlich die Prinzipien guter Programmierung anzuwenden, die man uns seitmehr als zwanzig Jahren beigebracht hat. C++ (Eiffel, Oberon-2, Smalltalk ...wählen Sie, was Sie wollen) ist die Neue Sprache, denn sie ist objekt-orientiert —aber Sie müssen sie nicht so benützen, wenn Sie nicht wollen (oder können), undes stellt sich heraus, daß man schlicht mit ANSI-C ebensoweit kommt. Nur Objekt-Orientierung ermöglicht es, Code von Projekt zu Projekt wiederzuverwenden — da-bei ist die Idee von Unterprogrammen fast so alt wie die Computer selbst, und guteProgrammierer haben schon immer ihre Werkzeuge und Bibliotheken wiederver-wendet.

Dieses Buch will objekt-orientierte Programmierung nicht verherrlichen und diealten Gewohnheiten nicht verdammen. Wir werden einfach ANSI-C benutzen, umzu entdecken, wie man objekt-orientiert programmiert, welche Techniken verwen-det werden, wieso sie uns helfen, größere Probleme zu lösen, wie man Allgemein-gültigkeit in den Griff bekommt und wie man so programmiert, daß man seine Feh-ler früher entdeckt. Dabei werden wir dem ganzen Jargon begegnen — Bindungen,Instanzen, Klassen, Methoden, Objekte, Polymorphismen, Vererbung und mehr —aber wir bleiben nicht im Bereich magischer Lösungen, sondern wir untersuchen,was dies mit dem zu tun hat, was wir schon immer getan und gewußt haben.

Zu entdecken, daß ANSI-C eine vollgültige objekt-orientierte Programmierspra-che ist, hat mir großen Spaß gemacht. Damit es Ihnen ebenso geht, sollten Sie mitANSI-C relativ gut umgehen können — mindestens müssen Sie leicht mit Struktu-ren, Zeigern, Prototypen und Funktionszeigern zurechtkommen. Auf Ihrem Wegdurch dieses Buch begegnen Sie dem ganzen Neusprech — nach Orwell und Web-ster eine Sprache ‘‘entworfen mit dem Ziel, die Breite des Denkens einzuschrän-ken’’ — und ich werde versuchen vorzuführen, daß hier nur die Prinzipien guter Pro-grammierung zu einer Methodik zusammengefaßt wurden, die Sie ohnehin schonimmer benutzen wollten. Es kann durchaus sein, daß Sie dann auch noch profes-sioneller mit ANSI-C umgehen.

Die ersten sechs Kapitel entwickeln die Grundlagen objekt-orientierter Program-mierung mit ANSI-C. Wir beginnen mit einer sorgfältigen Technik, um die Informati-on über abstrakte Datentypen zu verbergen. Dann fügen wir generische Funktionenhinzu, die auf dynamischer Bindung beruhen, und wir erben Code, indem wir Struk-turen behutsam verlängern. Schließlich kombinieren wir alles in einer Klassenhierar-chie, mit der wir unseren Code wesentlich leichter pflegen können.

Page 4: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

vi___________________________________________________________________________Vorwort

Zum Programmieren gehört Disziplin. Gut zu programmieren, braucht sehr vielDisziplin, eine größere Zahl von Prinzipien und standardisierte, defensive Methoden,um die Arbeit korrekt zu erledigen. Programmierer benutzen Werkzeuge. Gute Pro-grammierer bauen Werkzeuge, um Routinearbeiten ein für allemal abzuhaken. Zurobjekt-orientierten Programmierung mit ANSI-C benötigen wir eine ganze Menge sti-lisierten Code — die Namen können sich ändern, aber die Struktur bleibt gleich. Imsiebten Kapitel konstruieren wir deshalb einen kleinen Präprozessor ooc, der unsdie Standard-Codierung abnimmt. Das sieht dann zwar so aus wie noch ein neuerobjekt-orientierter Dialekt, aber es sollte nicht so eingeschätzt werden — derPräprozessor erledigt den langweiligen Teil, und wir können uns auf die kreativereSeite konzentrieren, nämlich anspruchsvollere Probleme mit besseren Techniken zulösen. ooc ist höchst flexibel: Wir haben das Programm entworfen und implemen-tiert, wir verstehen es und können es ändern, und es schreibt den gleichen ANSI-CCode, den wir auch von Hand produzieren würden.

Die anschließenden Kapitel verfeinern unsere Technologie. Im achten Kapitelfügen wir dynamische Typprüfung hinzu, damit wir Fehler früher finden. Im neun-ten Kapitel sorgen wir für automatische Initialisierung, um eine andere Klasse vonFehlern zu vermeiden. Im zehnten Kapitel beschäftigen wir uns mit den sogenann-ten Delegates und sehen, wie Klassen und Callback-Funktionen kooperieren, umzum Beispiel das wiederkehrende Problem zu vereinfachen, wie man Standard-Hauptprogramme konstruiert. Weitere Kapitel beschäftigen sich damit, Speicher-lecks mit Klassenmethoden zu stopfen, strukturierte Daten mit einer einheitlichenStrategie zu speichern und zu laden sowie mit einer Disziplin zur Reaktion auf Feh-ler, die auf verschachtelten Routinen zur Fehlerbehandlung beruht.

Im letzten Kapitel verlassen wir schließlich die reine Welt von ANSI-C und imple-mentieren den obligaten, mit der Maus bedienbaren Taschenrechner, und zwar zu-erst für die curses-Bibliothek und anschließend für das X Window System. DiesesBeispiel zeigt sehr schön, wie elegant wir mit Objekten und Klassen entwerfen undimplementieren können, selbst wenn wir mit der Unlogik fremder Bibliotheken undKlassenhierarchien fertigwerden müssen.

Jedes Kapitel hat am Schluß eine Zusammenfassung, wo ich versuche, vor al-lem für eiligere Leser in Kürze darzustellen, was jeweils erreicht wurde und wiewichtig es für die folgenden Kapitel ist. Das Buch präsentiert keine fertige objekt-orientierte Sprache, sondern es zeigt, wie man sich je nach Bedarf die richtigenTechniken selbst mit ANSI-C zurechtlegen kann. Deshalb steht am Schluß der mei-sten Kapitel auch noch ein Abschnitt, der Anstöße zu eigenen Überlegungen liefernsoll.

Ein wesentlicher Bestandteil dieses Buchs ist die beiliegende Diskette mit denProgrammquellen — sie enthält ein DOS-Dateisystem mit einem einzigen Shell-Pro-gramm, das alle Quellen nach Kapiteln geordnet erzeugt. Es gibt eine DateiReadMe, die Sie sich ansehen sollten, bevor Sie make aufrufen. Es ist auch rechtinstruktiv, wenn Sie mit einem Programm wie diff die Evolution der ooc-Reportsund der Wurzelklassen in den späteren Kapiteln verfolgen.

Page 5: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

vii___________________________________________________________________________Vorwort

Da wir alle Techniken vollständig entwickeln, habe ich keine eigene Klassenbi-bliothek konstruiert und benutzt, obgleich einige Beispiele durchaus davon profitierthätten. Wenn Sie objekt-orientierte Programmierung verstehen wollen, ist es mei-nes Erachtens wichtiger, daß Sie zuerst die Techniken meistern und Ihre Optionenim Design des Codes kennen; auf eine fremde Klassenbibliothek sollten Sie sicherst später verlassen.

Die Techniken, die in diesem Buch beschrieben werden, entstanden als Konse-quenz meiner Enttäuschung über C++, als ich Objekt-Orientierung benötigte, um ei-ne Dialogsprache zu implementieren, und feststellte, daß ich mit C++ keine portableLösung konstruieren konnte. Ich griff wieder zu dem, was ich beherrschte, nämlichANSI-C, und erreichte damit genau das, was ich brauchte. Ich habe die Technikeneiner Reihe von Teilnehmern in Vorlesungen und Kursen gezeigt, und auch anderehaben damit ihre Arbeiten erledigt. Das wäre dann auch schon alles gewesen —meine Fußnote zu einem Modetrend — wenn mich nicht Brian Kernighan undHans-Joachim Niclas dazu überredet hätten, meine Notizen zu publizieren (wasnatürlich dazu führte, daß ich alles nochmals neu erfand). Mein Dank gilt ihnen undallen andern, die mir geholfen und unter der Evolution dieses Buchs mitgelitten ha-ben. Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen nicht ersetzen.

Hollage, Pfingsten 1994Axel-Tobias Schreiner

Page 6: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 7: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

ix___________________________________________________________________________

Inhaltsverzeichnis

Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . v

1 Abstrakte Datentypen — Information verbergen . . . . . . . . . 1

1.1 Datentypen . . . . . . . . . . . . . . . . . . . . . 11.2 Abstrakte Datentypen . . . . . . . . . . . . . . . . . 11.3 Ein Beispiel — Set . . . . . . . . . . . . . . . . . . . 21.4 Speicherverwaltung . . . . . . . . . . . . . . . . . . 31.5 Object . . . . . . . . . . . . . . . . . . . . . . . 31.6 Eine Anwendung . . . . . . . . . . . . . . . . . . . 41.7 Eine Implementierung — Set . . . . . . . . . . . . . . . 51.8 Noch eine Implementierung — Bag . . . . . . . . . . . . 71.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . 91.10 Überlegungen . . . . . . . . . . . . . . . . . . . . . 10

2 Dynamische Bindung — Generische Funktionen . . . . . . . . . 11

2.1 Konstruktoren und Destruktoren . . . . . . . . . . . . . 112.2 Methoden, Nachrichten, Klassen und Objekte . . . . . . . . 122.3 Selektoren, dynamische Bindung und Polymorphismen . . . . . 132.4 Eine Anwendung . . . . . . . . . . . . . . . . . . . 162.5 Eine Implementierung — String . . . . . . . . . . . . . . 172.6 Noch eine Implementierung — Atom . . . . . . . . . . . . 192.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . 202.8 Überlegungen . . . . . . . . . . . . . . . . . . . . . 21

3 Programmierpraxis — Arithmetische Ausdrücke . . . . . . . . . 23

3.1 Die Hauptschleife . . . . . . . . . . . . . . . . . . . 233.2 Worte erkennen . . . . . . . . . . . . . . . . . . . . 243.3 Phrasen erkennen . . . . . . . . . . . . . . . . . . . 253.4 Ausdrücke verarbeiten . . . . . . . . . . . . . . . . . 253.5 Information verbergen . . . . . . . . . . . . . . . . . 263.6 Dynamische Bindung . . . . . . . . . . . . . . . . . . 273.7 Postfix ausgeben . . . . . . . . . . . . . . . . . . . 293.8 Arithmetik . . . . . . . . . . . . . . . . . . . . . . 303.9 Infix ausgeben . . . . . . . . . . . . . . . . . . . . 313.10 Zusammenfassung . . . . . . . . . . . . . . . . . . . 32

4 Vererbung — Code wiederverwenden und anpassen . . . . . . . 33

4.1 Eine Oberklasse — Point . . . . . . . . . . . . . . . . 334.2 Implementierung der Oberklasse — Point . . . . . . . . . . 344.3 Vererbung — Circle . . . . . . . . . . . . . . . . . . 354.4 Bindung und Vererbung . . . . . . . . . . . . . . . . . 374.5 Statische und dynamische Bindung . . . . . . . . . . . . 384.6 Sichtbarkeit und Zugriffsfunktionen . . . . . . . . . . . . 394.7 Implementierung der Unterklasse — Circle . . . . . . . . . 41

Page 8: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

x___________________________________________________________________________Inhaltsverzeichnis

4.8 Zusammenfassung . . . . . . . . . . . . . . . . . . . 434.9 Ist sie oder hat sie? — Vererbung kontra Aggregate . . . . . . 454.10 Mehrfache Vererbung . . . . . . . . . . . . . . . . . . 464.11 Überlegungen . . . . . . . . . . . . . . . . . . . . . 46

5 Programmierpraxis — Symboltabelle . . . . . . . . . . . . . 49

5.1 Namen erkennen . . . . . . . . . . . . . . . . . . . 495.2 Variablen . . . . . . . . . . . . . . . . . . . . . . . 505.3 Einträge in der Symboltabelle — Name . . . . . . . . . . . 515.4 Implementierung der Oberklasse — Name . . . . . . . . . 525.5 Implementierung der Unterklasse — Var . . . . . . . . . . 555.6 Zuweisung . . . . . . . . . . . . . . . . . . . . . . 565.7 Noch eine Unterklasse — Konstanten . . . . . . . . . . . 565.8 Mathematische Funktionen — Math . . . . . . . . . . . . 575.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . 595.10 Überlegungen . . . . . . . . . . . . . . . . . . . . . 60

6 Klassenhierarchie — Pflege vereinfachen . . . . . . . . . . . . 61

6.1 Forderungen . . . . . . . . . . . . . . . . . . . . . 616.2 Metaklassen . . . . . . . . . . . . . . . . . . . . . 626.3 Die Wurzeln — Object und Class . . . . . . . . . . . . . 636.4 Eine Unterklasse — Any . . . . . . . . . . . . . . . . 656.5 Implementierung — Object . . . . . . . . . . . . . . . 666.6 Implementierung — Class . . . . . . . . . . . . . . . . 676.7 Initialisierung . . . . . . . . . . . . . . . . . . . . . 696.8 Selektoren . . . . . . . . . . . . . . . . . . . . . . 706.9 Oberklassen-Selektoren . . . . . . . . . . . . . . . . . 716.10 Eine neue Metaklasse — PointClass . . . . . . . . . . . . 726.11 Zusammenfassung . . . . . . . . . . . . . . . . . . . 75

7 Der ooc Präprozessor — Codierstandards durchsetzen . . . . . . 79

7.1 Point nochmals betrachtet . . . . . . . . . . . . . . . . 797.2 Entwurf . . . . . . . . . . . . . . . . . . . . . . . 847.3 Präprozessor-Anweisungen . . . . . . . . . . . . . . . 867.4 Implementierungsstrategie . . . . . . . . . . . . . . . 877.5 Object nochmals betrachtet . . . . . . . . . . . . . . . 897.6 Diskussion . . . . . . . . . . . . . . . . . . . . . . 917.7 Ein Beispiel — List, Queue und Stack . . . . . . . . . . . 927.8 Überlegungen . . . . . . . . . . . . . . . . . . . . . 97

8 Dynamische Typprüfung — Defensiv programmieren . . . . . . . 99

8.1 Strategie . . . . . . . . . . . . . . . . . . . . . . . 998.2 Ein Beispiel — list . . . . . . . . . . . . . . . . . . . 1008.3 Implementierung . . . . . . . . . . . . . . . . . . . 1028.4 Codierstandard . . . . . . . . . . . . . . . . . . . . 1028.5 Rekursion vermeiden . . . . . . . . . . . . . . . . . . 1078.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . 1098.7 Überlegungen . . . . . . . . . . . . . . . . . . . . . 110

Page 9: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

xi___________________________________________________________________________Inhaltsverzeichnis

9 Statische Konstruktion — Selbstorganisation . . . . . . . . . . 111

9.1 Initialisierung . . . . . . . . . . . . . . . . . . . . . 1119.2 Initialisierungslisten — munch . . . . . . . . . . . . . . 1129.3 Funktionen für Objekte . . . . . . . . . . . . . . . . . 1149.4 Implementierung . . . . . . . . . . . . . . . . . . . 1169.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . 1189.6 Überlegungen . . . . . . . . . . . . . . . . . . . . . 119

10 Delegates — Callback-Funktionen . . . . . . . . . . . . . . . 121

10.1 Callbacks . . . . . . . . . . . . . . . . . . . . . . 12110.2 Abstrakte Basisklassen . . . . . . . . . . . . . . . . . 12210.3 Delegates . . . . . . . . . . . . . . . . . . . . . . 12310.4 Ein Rahmenprogramm — Filter . . . . . . . . . . . . . . 12510.5 Die Methode respondsTo . . . . . . . . . . . . . . . . 12810.6 Implementierung . . . . . . . . . . . . . . . . . . . 13010.7 Noch eine Anwendung — sort . . . . . . . . . . . . . . 13210.8 Zusammenfassung . . . . . . . . . . . . . . . . . . . 13410.9 Überlegungen . . . . . . . . . . . . . . . . . . . . . 135

11 Klassenmethoden — Lecks in der Speicherverwaltung . . . . . . 137

11.1 Ein Beispiel . . . . . . . . . . . . . . . . . . . . . . 13711.2 Klassenmethoden . . . . . . . . . . . . . . . . . . . 13911.3 Klassenmethoden implementieren . . . . . . . . . . . . . 14011.4 Programmierpraxis — Eine Dialogsprache mit Klasse(n) . . . . 14311.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . 15211.6 Überlegungen . . . . . . . . . . . . . . . . . . . . . 154

12 Persistente Objekte — Datenstrukturen speichern und laden . . . . 155

12.1 Ein Beispiel . . . . . . . . . . . . . . . . . . . . . . 15512.2 Objekte speichern — puto . . . . . . . . . . . . . . . . 16012.3 Objekte füllen — geto . . . . . . . . . . . . . . . . . 16212.4 Objekte laden — retrieve . . . . . . . . . . . . . . . . 16412.5 Objekte verbinden — value nochmals betrachtet . . . . . . . 16512.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . 16812.7 Überlegungen . . . . . . . . . . . . . . . . . . . . . 170

13 Exceptions — Fehlerbehandlung mit System . . . . . . . . . . 171

13.1 Strategie . . . . . . . . . . . . . . . . . . . . . . . 17113.2 Implementierung — Exception . . . . . . . . . . . . . . 17313.3 Beispiele . . . . . . . . . . . . . . . . . . . . . . 17513.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . 17813.5 Überlegungen . . . . . . . . . . . . . . . . . . . . . 178

14 Nachrichten weiterleiten — Ein GUI-Rechner . . . . . . . . . . 181

14.1 Die Idee — forward . . . . . . . . . . . . . . . . . . 18114.2 Implementierung . . . . . . . . . . . . . . . . . . . 18214.3 Ein Beispiel für objekt-orientiertes Entwerfen . . . . . . . . 18514.4 Implementierung — Ic und seine Unterklassen . . . . . . . . 188

Page 10: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

xii___________________________________________________________________________Inhaltsverzeichnis

14.5 Eine zeichenorientierte Schnittstelle — curses . . . . . . . . 19314.6 Eine grafische Schnittstelle — Xt . . . . . . . . . . . . . 19714.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . 20414.8 Überlegungen . . . . . . . . . . . . . . . . . . . . . 204

A ANSI-C Programmiertips . . . . . . . . . . . . . . . . . . . 207

A.1 Namen und Geltungsbereich . . . . . . . . . . . . . . . 207A.2 Funktionen . . . . . . . . . . . . . . . . . . . . . . 207A.3 Generische Zeiger — void * . . . . . . . . . . . . . . . 208A.4 const . . . . . . . . . . . . . . . . . . . . . . . . 209A.5 typedef und const . . . . . . . . . . . . . . . . . . . 210A.6 Strukturen . . . . . . . . . . . . . . . . . . . . . . 210A.7 Zeiger auf Funktionen . . . . . . . . . . . . . . . . . . 211A.8 Präprozessor . . . . . . . . . . . . . . . . . . . . . 212A.9 Verifikation — assert.h . . . . . . . . . . . . . . . . . 212A.10 Globale Sprünge — setjmp.h . . . . . . . . . . . . . . . 212A.11 Variable Parameterlisten — stdarg.h . . . . . . . . . . . . 213A.12 Datentypen — stddef.h . . . . . . . . . . . . . . . . . 213A.13 Speicherverwaltung — in stdlib.h . . . . . . . . . . . . . 214A.14 Memory-Funktionen — in string.h . . . . . . . . . . . . . 214

B Der ooc-Präprozessor — awk Programmiertips . . . . . . . . . 215

B.1 Architektur . . . . . . . . . . . . . . . . . . . . . . 215B.2 Dateimanagement — io.awk . . . . . . . . . . . . . . . 216B.3 Erkennung — parse.awk . . . . . . . . . . . . . . . . 216B.4 Die Datenbasis . . . . . . . . . . . . . . . . . . . . 217B.5 Reportgenerierung — report.awk . . . . . . . . . . . . . 218B.6 Zeilennumerierung . . . . . . . . . . . . . . . . . . . 220B.7 Das Hauptprogramm — main.awk . . . . . . . . . . . . . 221B.8 Reportdateien . . . . . . . . . . . . . . . . . . . . . 221B.9 Das Kommando ooc . . . . . . . . . . . . . . . . . . 222

C Manual . . . . . . . . . . . . . . . . . . . . . . . . . . 225

C.1 Kommandos . . . . . . . . . . . . . . . . . . . . . 225C.2 Funktionen . . . . . . . . . . . . . . . . . . . . . . 233C.3 Wurzelklassen . . . . . . . . . . . . . . . . . . . . 233C.4 Klassen im GUI-Rechner . . . . . . . . . . . . . . . . . 237

Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . 243

Page 11: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

1___________________________________________________________________________

1Abstrakte DatentypenInformation verbergen

1.1 DatentypenDatentypen sind integraler Bestandteil jeder Programmiersprache. ANSI-C hat int,double und char, um nur einige wenige zu nennen. Programmierern genügt selten,

was es schon gibt, und eine Programmiersprache bietet normalerweise die Mög-

lichkeit, neue Datentypen aus den vordefinierten zu konstruieren. Ein einfaches

Beispiel dafür sind Aggregate wie Vektoren, Strukturen oder Unions. Zeiger — nach

C. A. R. Hoare ‘‘ein Schritt zurück, von dem wir uns vielleicht nie erholen’’ — lassen

uns Daten von nahezu unbegrenzter Komplexität repräsentieren und manipulieren.

Was genau ist ein Datentyp? Wir können dies von verschiedenen Seiten be-

trachten. Ein Datentyp ist nur eine Menge von Werten — char hat normalerweise

256 verschiedene Werte, int hat wesentlich mehr; beide sind gleichförmig verteilt

und benehmen sich mehr oder weniger wie die natürlichen oder ganzen Zahlen der

Mathematik. double hat nochmals wesentlich mehr Werte, die sich aber keines-

wegs wie die reellen Zahlen der Mathematik verhalten.

Alternativ dazu können wir einen Datentyp als Wertemenge definieren, aber

kombiniert mit Operationen zu ihrer Manipulation. Typischerweise sind die Werte

das, was ein Computer repräsentieren kann, und die Operationen reflektieren mehr

oder weniger genau die verfügbaren Hardware-Befehle. Aus dieser Sicht benimmt

sich int in ANSI-C nicht besonders günstig: Die Wertemenge darf je nach Maschine

variieren, und Operationen wie eine arithmetische Verschiebung nach rechts dürfen

jeweils verschiedene Resultate liefern.

Komplizierteren Beispielen ergeht es nicht viel besser. Typischerweise würden

wir ein Element einer linearen Liste als Struktur vereinbaren:

typedef struct node {struct node * next;... Information ...

} node;

und für die Operationen deklarieren wir Funktionsköpfe wie

node * attach (node * elt, const node * tail);

Diese Lösung ist jedoch recht schlampig. Ein guter Programmierstil verlangt,

daß wir die Repräsentierung von Datenwerten verbergen und nur die möglichen

Manipulationen deklarieren.

1.2 Abstrakte DatentypenWir nennen einen Datentyp abstrakt , wenn wir seine Repräsentierung dem Benut-

zer nicht offenlegen. Theoretisch betrachtet, müssen wir dann die Eigenschaften

des Datentyps mit mathematischen Axiomen beschreiben, die die möglichen Ope-

Page 12: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

2___________________________________________________________________________1 Abstrakte Datentypen — Information verbergen

rationen verwenden. Beispielsweise können wir nur so oft ein Element aus einer

Warteschlange entfernen, wie wir zuvor eines eingefügt haben, und wir erhalten die

Elemente in der Reihenfolge zurück, in der wir sie eingegeben haben.

Abstrakte Datentypen bieten dem Programmierer sehr viel Flexibilität. Da die

Repräsentierung nicht Teil der Definition ist, können wir frei wählen, was gerade am

leichtesten oder effizientesten zu implementieren ist. Wenn wir erreichen, daß die

nötige Information korrekt verteilt ist, sind die Benutzung des Datentyps und die ge-

wählte Implementierung völlig unabhängig voneinander.

Abstrakte Datentypen befolgen Forderungen guten Programmierstils, nämlich

daß Information verborgen wird sowie das Prinzip ‘‘teile und herrsche.’’ Informati-

on, wie etwa die Repräsentierung einzelner Datenwerte, erhält nur der, der sie

wirklich wissen muß: der Implementierer, aber nicht der Benutzer. Mit einem ab-

strakten Datentyp können wir die Programmieraufgaben der Implementierung und

der Benutzung einwandfrei voneinander trennen: Damit sind wir schon weit vorge-

drungen, um ein großes System in kleine Module aufzuteilen.

1.3 Ein Beispiel — SetWie implementieren wir nun einen abstrakten Datentyp? Als Beispiel beschäftigen

wir uns mit einer Menge von Elementen mit den Operationen add, find und drop.*

Sie alle akzeptieren eine Menge und ein Element und liefern das Element, das zu

der Menge hinzugefügt, in ihr gefunden oder aus ihr entfernt wurde. Mit find kön-

nen wir eine Bedingung contains implementieren, die angibt, ob sich ein Element

schon in einer Menge befindet.

Aus dieser Perspektive ist Set ein abstrakter Datentyp. Um zu vereinbaren,

was wir mit einem Set tun können, schreiben wir eine Definitionsdatei Set.h:

#ifndef SET_H#define SET_H

extern const void * Set;

void * add (void * set, const void * element);void * find (const void * set, const void * element);void * drop (void * set, const void * element);int contains (const void * set, const void * element);

#endif

Die Präprozessor-Anweisungen schützen die Deklarationen: Egal wie oft wir Set.heinfügen, der C-Compiler sieht die Deklarationen nur einmal. Diese Technik zum

Schutz von Definitionsdateien ist so weit verbreitet, daß sie der C-Präprozessor von

GNU erkennt und nicht einmal mehr auf eine derart geschützte Datei zugreift, wenn

ihr Schutzsymbol bereits definiert ist.

Set.h ist vollständig, aber ist die Definitionsdatei brauchbar? Wir können kaum

weniger verraten oder annehmen: Set muß irgendwie die Tatsache repräsentieren,

____________________________________________________________________________________________

* Leider ist remove eine ANSI-C Bibliotheksfunktion, um eine Datei zu entfernen. Wenn wir diesen Na-

men für eine Set-Funktion verwenden würden, könnten wir nicht mehr auf stdio.h zugreifen.

Page 13: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

3___________________________________________________________________________1.4 Speicherverwaltung

daß wir mit Mengen arbeiten; add() akzeptiert ein Element, fügt es zu einer Menge

hinzu und liefert, was hinzugefügt wurde oder schon in der Menge vorhanden war;

find() liefert ein gesuchtes Element aus einer Menge oder einen Nullzeiger; drop()sucht ein Element, entfernt es aus einer Menge und liefert das entfernte Element;

contains() verwandelt das Resultat von find() in einen Wahrheitswert.

Wir verwenden überall den generischen Zeiger void *. Einerseits kann man da-

mit unmöglich herausfinden, wie eine Menge aussieht, andrerseits können wir aber

praktisch alles an add() und die anderen Funktionen übergeben. Nicht alles wird

sich wie eine Menge oder ein Element verhalten — wir verzichten auf die scheinba-

re Sicherheit einer Typprüfung, um dafür die Repräsentierung vollständig zu verber-

gen (information hiding). Wir werden jedoch im achten Kapitel sehen, daß unsere

Technik vollständig sicher gemacht werden kann.

1.4 SpeicherverwaltungWir haben vielleicht etwas übersehen: Wie erhalten wir eine Menge? Ein Set ist

ein Zeiger, kein mit typedef definierter Typ, folglich können wir keine lokalen oder

globalen Variablen vom Typ Set definieren. Wir werden statt dessen grundsätzlich

nur Zeiger verwenden, um auf Mengen und Elemente zu verweisen, und wir verein-

baren Quelle und Endlager aller Datenwerte vorläufig in new.h:

void * new (const void * type, ...);void delete (void * item);

Genau wie Set.h ist auch diese Definitionsdatei mit einem Präprozessor-Symbol

NEW_H geschützt. Im Buch sind nur die interessanten Teile jeder neuen Datei ab-

gedruckt, die Quelldiskette enthält den kompletten Text aller Beispiele — an dieser

Stelle sollten Sie die Datei c.01/new.h auf der Diskette ansehen.

new() erhält eine Typbeschreibung wie Set und vielleicht noch andere Argu-

mente zur Initialisierung und liefert einen Zeiger auf einen neuen Wert, dessen Re-

präsentierung zur Typbeschreibung paßt. delete() akzeptiert einen Zeiger, der ur-

sprünglich von new() stammen muß, und sorgt dafür, daß die damit verbundenen

Ressourcen wiederverwendet werden.

new() und delete() könnten eine Verkleidung der ANSI-C Funktionen calloc()und free() sein. In diesem Fall müßte die Typbeschreibung wenigstens festlegen,

wieviel Speicher für einen Wert gebraucht wird.

1.5 ObjectWenn wir irgend etwas Interessantes in einem Set speichern wollen, benötigen wir

einen weiteren abstrakten Datentyp Object, den die Definitionsdatei Object.h be-

schreibt:

extern const void * Object; /* new(Object); */

int differ (const void * a, const void * b);

differ() kann Object-Werte vergleichen: differ() ist wahr, wenn zwei Werte ver-

schieden sind, und falsch, wenn sie gleich sind. Diese Definition von differ() könn-

Page 14: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

4___________________________________________________________________________1 Abstrakte Datentypen — Information verbergen

te auch im Stil von strcmp() implementiert werden: Für manche Kombinationen

von Werten können wir negative oder positive Werte liefern, um eine Reihenfolge

festzulegen.

Objekte brauchen mehr Funktionalität, um wirklich nützlich zu sein. Im Moment

beschränken wir uns auf eine minimale Lösung, um die Zugehörigkeit zu einer Men-

ge zu untersuchen. Wenn wir eine größere Klassenbibliothek implementieren wür-

den, könnten wir feststellen, daß auch ein Set — und tatsächlich alles Andere —

ein Object-Wert ist und Mitglied einer Menge sein kann. An diesem Punkt erhält

man dann sehr viel Funktionalität praktisch umsonst.

1.6 Eine AnwendungMit den Definitionsdateien, das heißt, mit den Deklarationen der abstrakten Daten-

typen, können wir in main.c bereits eine Anwendung programmieren:

#include <stdio.h>

#include "new.h"#include "Object.h"#include "Set.h"

int main (){ void * s = new(Set);

void * a = add(s, new(Object));void * b = add(s, new(Object));void * c = new(Object);

if (contains(s, a) && contains(s, b))puts("ok");

if (contains(s, c))puts("contains?");

if (differ(a, add(s, a)))puts("differ?");

if (contains(s, drop(s, a)))puts("drop?");

delete(drop(s, b));delete(drop(s, c));

return 0;}

Wir erzeugen eine Menge und fügen zwei neue Object-Werte ein. Wenn alles

klappt, sollten wir die Werte in der Menge wiederfinden, und wir sollten keinen an-

deren, neuen Object-Wert entdecken. Das Programm sollte also schlicht ok ausge-

ben.

Der Aufruf von differ() illustriert einen semantischen Aspekt: Eine mathemati-

sche Menge kann einen bestimmten Object-Wert a nur einmal enthalten; wenn wir

versuchen, den Wert nochmals hinzuzufügen, sollten wir den ursprünglichen Wert

erhalten und differ() sollte falsch sein. Ebenso kann der Object-Wert nicht mehr in

der Menge sein, nachdem wir ihn einmal entfernt haben.

Page 15: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

5___________________________________________________________________________1.7 Eine Implementierung — ‘‘Set’’

Wenn wir ein Element entfernen, das sich nicht in der Menge befindet, überge-

ben wir in unserem Programm einen Nullzeiger an delete(). Dies soll zunächst ein-

mal — wie bei free() — erlaubt sein.

1.7 Eine Implementierung — Setmain.c kann zwar schon korrekt übersetzt werden, aber bevor wir das endgültige

Programm binden und ausführen können, müssen wir die abstrakten Datentypen

und die Speicherverwaltung implementieren. Wenn ein Object-Wert keine eigene

Information enthält und wenn jeder solche Wert höchstens zu einer Menge gehört,

können wir jeden Object-Wert und jedes Set als kleine, eindeutige, positive ganze

Zahlen repräsentieren, die wir als Indizes in einen Vektor heap[] verwenden. Wenn

ein Object-Wert zu einem Set gehört, speichern wir in seinem heap-Element die

Zahl, die das Set repräsentiert. Ein Object-Wert zeigt folglich auf das Set, zu dem

er gehört.

Diese erste Lösung ist so primitiv, daß wir alle Module in einer einzigen Datei

Set.c zusammenfassen. Mengen und ihre Elemente haben die gleiche Repräsentie-

rung, also ignoriert new() einfach die jeweilige Typbeschreibung Set oder Objectund liefert ein Element aus heap[], das mit dem Wert Null als frei markiert war:

#if ! defined MANY || MANY < 1#define MANY 10#endif

static int heap [MANY];

void * new (const void * type, ...){ int * p; /* & heap[1..] */

for (p = heap + 1; p < heap + MANY; ++ p)if (! * p)

break;assert(p < heap + MANY);* p = MANY;return p;

}

Null soll verfügbare Elemente von heap[] kennzeichnen; wir können daher keinen

Verweis auf heap[0] als Resultat liefern — wenn damit ein Set repräsentiert wird,

müßten seine Elemente den Index-Wert Null enthalten, und sie würden dann wie

freie Elemente von heap[] aussehen.

Wenn ein Object-Wert noch nicht zu einer Menge gehört, wird er mit dem un-

möglichen Index-Wert MANY markiert. Dann kann ihn new() nicht mehr als verfüg-

bar ansehen, und wir können ihn umgekehrt aber auch nicht als Mitglied irgendeiner

Menge interpretieren.

Unserer Funktion new() kann der Speicherplatz ausgehen. Dies ist der erste ei-

ner Vielzahl von Fehlern, die ‘‘nicht passieren können.’’ Wir verwenden einfach den

ANSI-C Makro assert(), um derartige Punkte im Code zu markieren. Eine realisti-

schere Implementierung sollte wenigstens eine vernünftige Fehlermeldung ausge-

Page 16: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

6___________________________________________________________________________1 Abstrakte Datentypen — Information verbergen

ben oder eine globale Funktion zur Fehlerbehandlung verwenden, die der Benutzer

bei Bedarf selbst definieren kann. Unser Ziel ist jedoch, eine Programmiertechnik

zu entwickeln, und wir wollen uns dabei auf das Wesentliche konzentrieren. Im Ka-

pitel 13 werden wir eine allgemeine Technik zur Behandlung von Fehlerbedingungen

betrachten.

delete() muß vor Nullzeigern auf der Hut sein. Ein Element von heap[] wird

wiederverwendbar gemacht, indem man es auf Null setzt:

void delete (void * _item){ int * item = _item;

if (item){ assert(item > heap && item < heap + MANY);

* item = 0;}

}

Wir müssen einheitlich mit generischen Zeigern umgehen. Wir lassen die Parame-

ternamen mit einem Unterstrich beginnen und benützen sie nur dazu, lokale Varia-

blen mit den gewünschten Typen und den eigentlichen Namen zu initialisieren.

Eine Menge wird in ihren Elementen repräsentiert: Jedes Element zeigt auf

seine Menge. Wenn ein Element in heap[] noch den Wert MANY enthält, kann es

zu einer Menge hinzugefügt werden; andernfalls sollte es schon in der Menge sein,

denn wir erlauben nicht, daß ein Object-Wert zu mehr als einer Menge gehört.

void * add (void * _set, const void * _element){ int * set = _set;

const int * element = _element;

assert(set > heap && set < heap + MANY);assert(* set == MANY);assert(element > heap && element < heap + MANY);

if (* element == MANY)* (int *) element = set - heap;

elseassert(* element == set - heap);

return (void *) element;}

assert() sorgt für eine gewisse Rückversicherung: Wir sollten uns nur mit Zeigern

in heap[] beschäftigen, und ein Set-Wert sollte selbst zu keiner Menge gehören,

das heißt, sein Elementwert in heap[] kann nur MANY sein.

Die anderen Funktionen sind genauso einfach. find() untersucht nur, ob sein

Element den richtigen Index für die Menge enthält:

void * find (const void * _set, const void * _element){ const int * set = _set;

const int * element = _element;

assert(set > heap && set < heap + MANY);assert(* set == MANY);assert(element > heap && element < heap + MANY);assert(* element);

Page 17: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

7___________________________________________________________________________1.8 Noch eine Implementierung — ‘‘Bag’’

return * element == set - heap ? (void *) element : 0;}

contains() verwandelt das Resultat von find():int contains (const void * _set, const void * _element){

return find(_set, _element) != 0;}

drop() kann mit find() prüfen, ob das Element wirklich in der Menge ist, aus der es

entfernt werden soll. Falls ja, markieren wir es mit MANY und verwandeln es damit

zurück in einen einfachen Object-Wert:

void * drop (void * _set, const void * _element){ int * element = find(_set, _element);

if (element)* element = MANY;

return element;}

Wir könnten strenger sein und verlangen, daß das zu entfernende Element zu kei-

ner anderen Menge gehört, aber dann würden wir viel von find() in drop() noch-

mals programmieren.

Unsere Implementierung ist recht unkonventionell. Es stellt sich heraus, daß

wir differ() gar nicht benötigen, um eine Menge zu implementieren. Wir müssen

die Funktion aber trotzdem realisieren, denn sie wird in unserer Anwendung einge-

setzt.

int differ (const void * a, const void * b){

return a != b;}

Zwei Object-Werte sind genau dann verschieden, wenn sie mit verschiedenen In-

dexwerten repräsentiert werden, das heißt, wir vergleichen einfach zwei Zeigerwer-

te.

Wir sind schon fertig — in dieser Lösung haben wir die Typbeschreibungen Setund Object nicht einmal verwendet, aber wir müssen sie trotzdem definieren, da-

mit der C-Compiler nicht meckert:

const void * Set;const void * Object;

Wir haben diese Zeiger in main() angegeben, um neue Mengen und Elemente zu

erzeugen.

1.8 Noch eine Implementierung — BagOhne die sichtbare Schnittstelle in Set.h zu verändern, können wir ganz anders im-

plementieren. Diesmal verwenden wir dynamische Speicherbereiche und repräsen-

tieren Mengen und Elemente als Strukturen:

Page 18: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

8___________________________________________________________________________1 Abstrakte Datentypen — Information verbergen

struct Set { unsigned count; };struct Object { unsigned count; struct Set * in; };

In .count notieren wir die Anzahl Elemente in einer Menge. In einem Element gibt

.count an, wie oft das Element zur Menge hinzugefügt wurde. Wenn wir .count je-

weils verringern, wenn das Element an drop() übergeben wird, und das Element

nur entfernen, wenn .count null ist, erhalten wir einen Bag, das heißt, eine Menge,

deren Elemente Verweiszähler besitzen und daher mehrfach in der Menge vorkom-

men können.

Da wir jetzt dynamischen Speicher verwenden, um Mengen und Elemente zu

repräsentieren, müssen wir die Typbeschreibungen Set und Object so initialisieren,

daß new() herausfinden kann, wieviel Speicher reserviert werden muß:

static const size_t _Set = sizeof(struct Set);static const size_t _Object = sizeof(struct Object);

const void * Set = & _Set;const void * Object = & _Object;

new() ist jetzt viel einfacher:

void * new (const void * type, ...){ const size_t size = * (const size_t *) type;

void * p = calloc(1, size);

assert(p);return p;

}

delete() kann sein Argument direkt an free() übergeben — in ANSI-C darf man einen

Nullzeiger an free() übergeben.

add() kann seine Zeigerargumente kaum überprüfen. Wir vergrößern den Ver-

weiszähler im Element und die Anzahl der Elemente in der Menge:

void * add (void * _set, const void * _element){ struct Set * set = _set;

struct Object * element = (void *) _element;

assert(set);assert(element);

if (! element -> in)element -> in = set;

elseassert(element -> in == set);

++ element -> count, ++ set -> count;

return element;}

find() kontrolliert noch immer, ob sein Element auf die richtige Menge zeigt:

void * find (const void * _set, const void * _element){ const struct Object * element = _element;

assert(_set);assert(element);

Page 19: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

9___________________________________________________________________________1.9 Zusammenfassung

return element -> in == _set ? (void *) element : 0;}

contains() beruht auf find() und bleibt unverändert.

Wenn wir bei drop() das Element in der richtigen Menge finden, verringern wir

den Verweiszähler und die Anzahl der Elemente in der Menge. Ist der Verweiszäh-

ler dann null, entfernen wir das Element aus der Menge:

void * drop (void * _set, const void * _element){ struct Set * set = _set;

struct Object * element = find(set, _element);

if (element){ if (-- element -> count == 0)

element -> in = 0;-- set -> count;

}return element;

}

Wir können jetzt eine neue Funktion count() einführen, die die Anzahl Elemente

einer Menge liefert:

unsigned count (const void * _set){ const struct Set * set = _set;

assert(set);return set -> count;

}

Es wäre natürlich einfacher, wenn wir eine Anwendung die Komponente .count aus

einem Set-Wert direkt lesen lassen, aber wir bestehen darauf, daß wir die Reprä-

sentierung einer Menge nicht offenlegen. Der Aufwand für einen Funktionsaufruf

ist gering im Vergleich zur Gefahr, daß eine Anwendung einen kritischen, internen

Wert verändern kann, wenn sie die Repräsentierung eines Datentyps kennt.

Ein Bag verhält sich anders als ein Set: Ein Element kann mehr als einmal hin-

zugefügt werden und verschwindet dann nur, wenn es so oft entfernt wird, wie es

hinzugefügt wurde. Unsere Anwendung im Abschnitt 1.6 hat den Object-Wert azweimal hinzugefügt. Wenn der Wert einmal entfernt wird, findet ihn contains()noch immer im Bag. Jetzt liefert das Testprogramm die Ausgabe

okdrop?

1.9 ZusammenfassungFür einen abstrakten Datentyp verbergen wir sämtliche Aspekte seiner Implemen-

tierung, wie zum Beispiel die Repräsentierung der einzelnen Werte, vor dem An-

wendungsprogramm.

Die Anwendung hat nur Zugriff auf eine Definitionsdatei, in der ein Zeiger den

Datentyp repräsentiert, und in der Operationen für den Datentyp als Funktionen de-

klariert sind, die generische Zeiger akzeptieren und liefern.

Page 20: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

10___________________________________________________________________________1 Abstrakte Datentypen — Information verbergen

Mit einem Zeiger auf die Typbeschreibung liefert eine allgemeine Funktion

new() einen Zeiger auf einen Wert, den man später an eine allgemeine Funktion

delete() übergibt, um die zugehörigen Ressourcen wieder freizugeben.

Normalerweise wird jeder abstrakte Datentyp in einer eigenen Quelldatei imple-

mentiert. Idealerweise hat er keinen Zugriff auf die Repräsentierung anderer Daten-

typen. Die Typbeschreibung enthält normalerweise mindestens einen konstanten

size_t-Wert, der den Platzbedarf eines Werts angibt.

1.10 ÜberlegungenWenn ein Element gleichzeitig zu mehreren Mengen gehören kann, benötigen wir

eine andere Repräsentierung für eine Menge. Wenn wir Object-Werte nach wie

vor als kleine, eindeutige, ganze Zahlen darstellen und wenn wir die Anzahl der

möglichen Objekte begrenzen, können wir eine Menge als Bitfolge in einer langen

Zeichenkette repräsentieren, in der ein Bit durch den Elementwert ausgewählt wird

und gesetzt oder gelöscht ist, je nachdem, ob das Element zur Menge gehört oder

nicht.

Eine allgemeinere und konventionellere Lösung repräsentiert eine Menge als li-

neare Liste von Knoten, in denen die Adressen der Mengenelemente gespeichert

werden. Damit ergeben sich keine Einschränkungen für die Elemente, und eine

Menge kann implementiert werden, ohne daß man die Repräsentierung eines Ele-

ments kennt.

Die Fehlersuche wird sehr erleichtert, wenn man einzelne Elemente betrachten

kann. Eine relativ allgemeine Lösung besteht aus zwei Funktionen

int store (const void * object, FILE * fp);int storev (const void * object, va_list ap);

store() gibt eine Beschreibung eines Object-Werts über den FILE-Zeiger aus.

storev() holt mit va_arg() den FILE-Zeiger aus der Argumentliste, auf die ap zeigt.

Beide Funktionen liefern die Anzahl der ausgegebenen Zeichen. storev() ist sehr

praktisch, wenn wir folgende Funktion für Mengen implementieren:

int apply (const void * set,int (* action) (void * object, va_list ap), ...);

apply() ruft action() für jedes Element in set auf und übergibt den Rest der Argu-

mentliste. action() darf set nicht ändern, kann aber Null liefern, um apply() vorzei-

tig zu beenden. apply() liefert einen von Null verschiedenen Wert, wenn alle Ele-

mente bearbeitet wurden.

Page 21: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

11___________________________________________________________________________

2Dynamische Bindung

Generische Funktionen

2.1 Konstruktoren und DestruktorenWir wollen eine einfache Art von Zeichenketten implementieren, die wir später zu-sammen mit Mengen verwenden können. Für eine neue Zeichenkette legen wireinen dynamischen Puffer an, der den Text enthält. Wenn die Zeichenkettegelöscht wird, müssen wir den Puffer freigeben.

new() muß einen Wert erzeugen, und delete() muß die Ressourcen freigeben,die der Wert besitzt. new() weiß, was für ein Wert erzeugt wird, denn diese Funkti-on hat eine Typbeschreibung als ersten Parameter. In Abhängigkeit von diesem Pa-rameter könnten wir eine Kette von if-Anweisungen verwenden, um jede Art vonWert individuell zu behandeln. Der Nachteil ist dabei allerdings, daß new() dann ex-plizit Code für jede Typbeschreibung enthält, die wir unterstützen.

delete() hat jedoch ein größeres Problem. Auch diese Funktion muß sich jenach Argumentwert anders verhalten: Bei einer Zeichenkette muß ihr Puffer freige-geben werden, bei einem Object-Wert aus dem ersten Kapitel muß nur die Flächefür den Wert selbst freigegeben werden, und ein Set hat vielleicht verschiedeneSpeicherbereiche zusammengetragen, in denen Verweise auf die Elemente stehen.

Wir könnten für delete() einen zusätzlichen Parameter einführen: entwederebenfalls unsere Typbeschreibung, oder die Funktion, die die Aufräumungsarbeitendurchführt. Diese Lösung ist aber unelegant und fehleranfällig. Es gibt eine we-sentlich allgemeinere und elegante Technik: Jeder Wert muß selbst wissen, wie erseine Ressourcen freigibt. Zu jedem einzelnen Objekt gehört ein Zeiger, mit demwir eine Aufräumfunktion finden können. Wir nennen eine derartige Funktion einenDestruktor für den Wert.

Jetzt hat new() ein Problem. Die Funktion muß Werte erzeugen und Zeiger lie-fern, die an delete() übergeben werden können, das heißt, new() muß in jedenWert die Destruktor-Information eintragen. Die offensichtliche Lösung besteht dar-in, einen Zeiger auf den Destruktor in die Typbeschreibung einzutragen, den wir oh-nehin an new() übergeben. Bisher brauchen wir etwa folgende Deklarationen:

struct type {size_t size; /* Groesse eines Objekts */void (* dtor) (void *); /* Destruktor */

};struct String {

char * text; /* dynamischer Puffer */const void * destroy; /* Verweis auf Destruktor */

};

Page 22: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

12___________________________________________________________________________2 Dynamische Bindung — Generische Funktionen

struct Set {... Information ...const void * destroy; /* Verweis auf Destruktor */

};Wir haben anscheinend wieder ein Problem: Jemand muß den Destruktor-Zeigerdtor aus der Typbeschreibung nach destroy im neuen Wert kopieren, und mögli-cherweise muß die Kopie je nach Art der Werte an eine andere Stelle geschriebenwerden.

new() muß auch die Initialisierung der neuen Werte übernehmen. Für verschie-dene Typen bedeutet das verschiedene Arbeit — new() braucht möglicherweise so-gar verschiedene Argumente, um verschiedene Typen von Werten zu erzeugen:

new(Set); /* eine Menge */new(String, "text"); /* eine Zeichenkette */

Zur Initialisierung verwenden wir eine weitere typspezifische Funktion, die wir alsKonstruktor bezeichnen. Da Konstruktor und Destruktor vom Typ abhängen undsich nicht ändern, übergeben wir sie beide als Teil der Typbeschreibung an new().

Man sollte beachten, daß Konstruktor und Destruktor nicht dafür verantwortlichsind, den Speicher für den Wert selbst anzulegen oder freizugeben — darum müs-sen sich new() und delete() kümmern. Der Konstruktor wird von new() aufgerufenund muß nur den Speicherbereich initialisieren, den new() angelegt hat. Für eineZeichenkette muß der Konstruktor wirklich eine weitere Speicherfläche beschaffenund den Text darin speichern, aber der Platz für struct String selbst wird durchnew() bereitgestellt. Dieser Platz wird dann später durch delete() freigegeben. Zu-erst ruft jedoch delete() den Destruktor auf, der im wesentlichen die Initialisierungrückgängig macht, die der Konstruktor vorgenommen hat, bevor dann delete() denSpeicherbereich zur Wiederverwendung freigibt, den new() angelegt hat.

2.2 Methoden, Nachrichten, Klassen und Objektedelete() muß den Destruktor finden können, ohne zunächst zu wissen, was für eineArt von Wert übergeben wurde. Wir überarbeiten deshalb die Deklarationen ausAbschnitt 2.1, denn wir müssen darauf bestehen, daß sich der Zeiger, mit dem wirden Destruktor finden können, am Anfang aller Werte befindet, die an delete()übergeben werden, egal welchen Typ diese Werte besitzen.

Worauf soll dieser Zeiger zeigen? Wenn wir nur die Adresse eines Werts besit-zen, gibt uns dieser Zeiger Zugriff auf typspezifische Information für den Wert, wiezum Beispiel seinen Destruktor. Wahrscheinlich werden wir bald noch andere typ-spezifische Funktionen erfinden, wie zum Beispiel eine Funktion, mit der wir Werteausgeben können, oder unsere Vergleichsfunktion differ() oder eine Funktionclone(), um eine vollständige Kopie eines Werts zu erzeugen. Wir verwenden folg-lich einen Zeiger auf eine Tabelle von Funktionszeigern.

Genau betrachtet muß diese Tabelle Teil der Typbeschreibung sein, die annew() übergeben wird, und die offensichtliche Lösung ist, daß ein Wert einfach aufdie ganze Typbeschreibung zeigt.

Page 23: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

13___________________________________________________________________________2.3 Selektoren, dynamische Bindung und Polymorphismen

struct Class {size_t size;void * (* ctor) (void * self, va_list * app);void * (* dtor) (void * self);void * (* clone) (const void * self);int (* differ) (const void * self, const void * b);

};struct String {

const void * class; /* unbedingt zuerst */char * text;

};struct Set {

const void * class; /* unbedingt zuerst */...

};Jeder unserer Werte beginnt mit einem Zeiger auf seine eigene Typbeschreibung,und über diese Tabelle können wir typspezifische Information für den Wert finden:.size ist die Anzahl Bytes, die new() für den Wert bereitstellt; .ctor zeigt auf denKonstruktor, den new() aufruft und der die neue Speicherfläche und den Rest derursprünglichen Argumentliste von new() erhält; .dtor zeigt auf den Destruktor, dendelete() aufruft und der den Wert erhält, der freigegeben wird; .clone zeigt auf eineKopierfunktion, die einen Wert erhält, der kopiert werden muß; .differ schließlichzeigt auf eine Funktion, die ihren Wert mit irgendeinem anderen vergleicht.

Betrachten wir diese Liste, so stellen wir fest, daß jede Funktion für den Wertarbeitet, mit dessen Hilfe sie ausgewählt wird. Nur der Konstruktor muß sich viel-leicht mit einer nur teilweise initialisierten Speicherfläche auseinandersetzen. Wirnennen derartige Funktionen Methoden und die Werte mit TypbeschreibungenObjekte. Der Aufruf einer Methode ist eine Nachricht, und der Parametername selfkennzeichnet den Empfänger der Nachricht, das heißt, das Objekt, durch dessenTypbeschreibung die Methode gewählt wird. Da wir nur gewöhnliche C-Funktionenverwenden, muß self zwar unbedingt ein, aber nicht immer der erste, Parametersein.

Viele Objekte werden die gleiche Typbeschreibung verwenden, das heißt, siebenötigen gleich viel Speicherplatz für das Objekt selbst, und die gleichen Metho-den können auf sie angewendet werden. Wir nennen alle Objekte mit der gleichenTypbeschreibung eine Klasse, und ein einzelnes Objekt wird auch als Instanz derKlasse bezeichnet. Bisher sind eine Klasse, ein abstrakter Datentyp und eine Wer-temenge mit Operationen, das heißt, ein Datentyp, so ziemlich dasselbe.

Ein Objekt ist Instanz einer Klasse, das heißt, es hat einen eigenen Zustand, derin dem durch new() beschafften Speicher repräsentiert und mit den Methoden derKlasse manipuliert wird. Im konventionellen Sprachgebrauch ist ein Objekt durch-aus nur ein Wert eines bestimmten Datentyps.

2.3 Selektoren, dynamische Bindung und PolymorphismenWer kümmert sich um die Nachrichten? Der Konstruktor wird von new() mit einerneuen Speicherfläche aufgerufen, die nahezu uninitialisiert ist:

Page 24: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

14___________________________________________________________________________2 Dynamische Bindung — Generische Funktionen

void * new (const void * _class, ...){ const struct Class * class = _class;

void * p = calloc(1, class -> size);assert(p);* (const struct Class **) p = class;if (class -> ctor){ va_list ap;

va_start(ap, _class);p = class -> ctor(p, & ap);va_end(ap);

}return p;

}Die Existenz des Zeigers auf struct Class am Anfang eines Objekts ist außerordent-lich wichtig. Deshalb initialisieren wir diesen Zeiger bereits in new():

•p

Objekt

...........

class

size

ctor

dtor

clone

differ

struct Class

Die Typbeschreibung class ganz rechts wird vorläufig bereits in der Übersetzung in-itialisiert. Das Objekt wird zur Laufzeit erzeugt, und dann werden die gestricheltenZeiger eingetragen. In der Zuweisung

* (const struct Class **) p = class;zeigt p auf den Anfang der neuen Speicherfläche für das Objekt. Wir erzwingen ei-ne Umwandlung von p, die den Anfang des Objekts als Zeiger auf struct Class in-terpretiert, und tragen class als Wert dieses Zeigers ein.

Wenn dann ein Konstruktor Teil der Typbeschreibung ist, rufen wir ihn auf undliefern sein Resultat als Resultat von new(), das heißt, als neues Objekt. Abschnitt2.6 illustriert, daß ein trickreicher Konstruktor dadurch sogar seine eigene Speicher-verwaltung einführen kann.

Nur explizit sichtbare Funktionen wie new() können eine variable Parameterlistebesitzen. Auf die Argumentliste wird mit einer va_list-Variablen ap zugegriffen, diemit dem Makro va_start() aus stdarg.h initialisiert wird. new() kann nur die ganzeListe an den Konstruktor übergeben; deshalb deklarieren wir .ctor mit einem Para-meter vom Typ va_list und nicht mit seiner eigenen variablen Parameterliste. Dawir später vielleicht die ursprünglichen Parameter unter verschiedenen Funktionen

Page 25: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

15___________________________________________________________________________2.3 Selektoren, dynamische Bindung und Polymorphismen

aufteilen, übergeben wir die Adresse von ap an den Konstruktor — nach dem Auf-ruf kann dann ap auf das erste Argument zeigen, das der Konstruktor nicht ausge-wertet hat.

delete() nimmt an, daß jedes Objekt, das heißt, jeder von Null verschiedeneZeiger, auf eine Typbeschreibung zeigt. Mit diesem Zeiger wird der Destruktor ge-funden, falls einer existiert. Hier spielt self die Rolle von p in der vorher betrachte-ten Abbildung. Wir erzwingen die Umwandlung mit einer lokalen Variablen cp undtasten uns ganz vorsichtig von self zu seiner Beschreibung vor:

void delete (void * self){ const struct Class ** cp = self;

if (self && * cp && (* cp) -> dtor)self = (* cp) -> dtor(self);

free(self);}

Auch der Destruktor könnte einen neu berechneten Zeiger als Resultat liefern, da-mit ihn delete() an free() übergibt. Wenn der Konstruktor in der Speicherverwal-tung gemogelt hat, kann der Destruktor dies wieder ausgleichen, siehe Abschnitt2.6. Wenn ein Objekt nicht zerstört werden will, kann sein Destruktor einen Nullzei-ger liefern.

Alle anderen Methoden, die in der Typbeschreibung gespeichert sind, werdenähnlich aufgerufen. Jedesmal haben wir einen einzigen Empfänger self, und wirmüssen die Nachricht, also den Aufruf der Methode, mit Hilfe der Typbeschreibungim Empfänger verschicken:

int differ (const void * self, const void * b){ const struct Class * const * cp = self;

assert(self && * cp && (* cp) -> differ);return (* cp) -> differ(self, b);

}Kritisch ist natürlich die Annahme, daß wir einen Zeiger auf eine Typbeschreibung* self direkt unter einem beliebigen Zeiger self vorfinden. Im Moment schützen wiruns wenigstens gegen Nullzeiger. Wir könnten jede Typbeschreibung mit einer‘‘magischen Zahl’’ beginnen lassen oder sogar * self mit den Adressen oder wenig-stens einem Adreßbereich aller bekannten Typbeschreibungen vergleichen, aber wirwerden im achten Kapitel sehen, daß wir noch wesentlich sorgfältiger prüfen kön-nen.

Auf jeden Fall illustriert differ(), warum diese Technik zum Funktionsaufrufdynamische oder späte Bindung genannt wird: Wir können zwar eine Funktion mitNamen differ() für beliebige Objekte aufrufen, wenn sie nur mit einem Zeiger auf ei-ne geeignete Typbeschreibung beginnen, aber die Funktion, die die eigentliche Ar-beit tut, wird so spät wie möglich ausgewählt — erst bei Ausführung des Aufrufs,nicht vorher.

Wir werden differ() selbst als Selektor-Funktion bezeichnen. Es ist ein Beispieleiner polymorphen Funktion, das heißt, einer Funktion, die Argumente verschiede-nen Typs akzeptiert und sie in Abhängigkeit vom Typ verschieden bearbeitet. Wenn

Page 26: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

16___________________________________________________________________________2 Dynamische Bindung — Generische Funktionen

wir mehrere Klassen implementieren, die alle .differ in ihren Typbeschreibungenenthalten, ist differ() eine generische Funktion, die auf beliebige Objekte aus allendiesen Klassen angewendet werden kann.

Wir können Selektoren als Methoden ansehen, die selbst zwar nicht dynamischgebunden sind, die sich aber trotzdem wie polymorphe Funktionen verhalten, dennsie lassen dynamisch gebundene Funktionen ihre eigentliche Arbeit tun.

Polymorphe Funktionen sind tatsächlich in vielen Programmiersprachen vordefi-niert, zum Beispiel verarbeitet die Prozedur write() in Pascal verschiedene Argu-menttypen verschieden, und der Operator + in C hat verschiedene Effekte, je nach-dem, ob er auf Integer-, Zeiger- oder Gleitkommawerte angewendet wird. Vor al-lem bei Operatoren nennt man dieses Phänomen Overloading (überladen): Die Ar-gumenttypen und der Operator legen nur zusammen fest, was der Operator be-wirkt; der gleiche Operator kann mit verschiedenen Argumenttypen verwendet wer-den und hat dann verschiedene Effekte.

Diese Dinge kann man nicht klar unterscheiden: Durch dynamische Bindungverhält sich differ() wie eine überladene Funktion, und der C-Compiler kann + sichso benehmen lassen wie eine polymorphe Funktion — wenigstens für die vordefi-nierten Datentypen. Allerdings kann nur der C-Compiler für verschiedene Aufrufedes Operators + verschiedene Resultattypen liefern; die Funktion differ() muß unab-hängig von ihren Argumenten immer den gleichen Resultattyp besitzen. Ein Objektals Resultat bietet natürlich immer noch eine sehr hohe Flexibilität.

Methoden können sich polymorph verhalten, ohne dynamische Bindung zu be-sitzen. Als Beispiel betrachten wir eine Funktion sizeOf(), die die Größe eines belie-bigen Objekts liefert:

size_t sizeOf (const void * self){ const struct Class * const * cp = self;

assert(self && * cp);return (* cp) -> size;

}Alle Objekte verweisen auf ihre Typbeschreibung, und wir können die Größe vondort beschaffen. Es gibt hier übrigens einen feinen Unterschied:

void * s = new(String, "text");assert(sizeof s != sizeOf(s));

sizeof ist ein C-Operator, der beim Übersetzen bewertet wird, und der die AnzahlBytes liefert, die sein Argument benötigt. sizeOf() ist unsere polymorphe Funktion,die zur Laufzeit die Anzahl Bytes für das Objekt liefert, auf das ihr Argument zeigt.

2.4 Eine AnwendungWir haben zwar die Klasse String für Zeichenketten noch nicht implementiert, aberwir können durchaus schon ein einfaches Testprogramm schreiben. String.h verein-bart den abstrakten Datentyp:

extern const void * String;

Page 27: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

17___________________________________________________________________________2.5 Eine Implementierung — ‘‘String’’

Alle unsere Methoden beziehen sich (noch) auf alle Objekte, deshalb vereinbarenwir sie vorläufig in new.h, der Definitionsdatei der Speicherverwaltung, die im Ab-schnitt 1.4 eingeführt wurde:

void * clone (const void * self);int differ (const void * self, const void * b);size_t sizeOf (const void * self);

Die ersten beiden Prototypen deklarieren Selektoren. Wir leiten sie von den zu-gehörigen Komponenten aus struct Class ab, indem wir einfach eine Verweis-ebene, also einen *, aus dem Deklarator entfernen. Hier ist das Testprogramm:

#include "String.h"#include "new.h"int main (){ void * a = new(String, "a"), * aa = clone(a);

void * b = new(String, "b");printf("sizeOf(a) == %u\n", sizeOf(a));if (differ(a, b))

puts("ok");if (differ(a, aa))

puts("differ?");if (a == aa)

puts("clone?");delete(a), delete(aa), delete(b);return 0;

}Wir erzeugen zwei Zeichenketten und kopieren eine davon. Wir zeigen die Größeeines String-Objekts — nicht die Länge des Texts, den das Objekt kontrolliert —und wir kontrollieren, daß aus verschiedenen Texten auch verschiedene String-Objekte entstehen. Schließlich prüfen wir nach, daß die Kopie zwar gleich abernicht identisch zum Original ist, und wir geben die Objekte wieder frei. Wenn allesfunktioniert, wird das Programm ungefähr Folgendes ausgeben:

sizeOf(a) == 8ok

2.5 Eine Implementierung — StringWir implementieren die String-Klasse, indem wir die Methoden schreiben, die indie Typbeschreibung String eingetragen werden. Die dynamische Bindung legtfest, welche Funktionen geschrieben werden müssen, um einen neuen Datentyp zuimplementieren.

Der Konstruktor holt sich den Text, der an new() übergeben wurde, und verbin-det eine dynamische Kopie mit der struct String, die new() bereitgestellt hat:

struct String {const void * class; /* unbedingt zuerst */char * text;

};

Page 28: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

18___________________________________________________________________________2 Dynamische Bindung — Generische Funktionen

static void * String_ctor (void * _self, va_list * app){ struct String * self = _self;

const char * text = va_arg(* app, const char *);self -> text = malloc(strlen(text) + 1);assert(self -> text);strcpy(self -> text, text);return self;

}Im Konstruktor müssen wir nur .text initialisieren, denn new() hat die Typbeschrei-bung schon in .class eingetragen.

Der Destruktor gibt den dynamischen Speicher frei, den das String-Objekt kon-trolliert. Da delete() den Destruktor nur aufrufen kann, wenn self von Null verschie-den ist, brauchen wir nichts zu überprüfen:

static void * String_dtor (void * _self){ struct String * self = _self;

free(self -> text), self -> text = 0;return self;

}String_clone() soll eine Zeichenkette kopieren. Da wir später Original wie Ko-

pie an delete() übergeben, müssen wir eine neue, dynamische Kopie des Texts imString-Objekt anlegen. Dies geht sehr leicht mit Hilfe von new():

static void * String_clone (const void * _self){ const struct String * self = _self;

return new(String, self -> text);}

String_differ() ist sicher falsch, wenn wir identische String-Objekte betrach-ten, und das Resultat ist wahr, wenn wir eine Zeichenkette mit einem ganz anderenObjekt vergleichen. Wenn wir wirklich zwei verschiedene String-Objekte verglei-chen, verwenden wir strcmp():

static int String_differ (const void * _self, const void * _b){ const struct String * self = _self;

const struct String * b = _b;if (self == b)

return 0;if (! b || b -> class != String)

return 1;return strcmp(self -> text, b -> text);

}Typbeschreibungen sind eindeutig — damit finden wir hier heraus, ob unser zweitesArgument wirklich ein String-Objekt ist.

Alle diese Methoden werden static definiert, denn sie sollen nur über new(),delete() oder die Selektoren aufgerufen werden. Die Selektoren erreichen die Me-thoden über die Typbeschreibung:

Page 29: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

19___________________________________________________________________________2.6 Noch eine Implementierung — ‘‘Atom’’

#include "new.r"static const struct Class _String = {

sizeof(struct String),String_ctor, String_dtor,String_clone, String_differ

};const void * String = & _String;

String.c benützt die öffentlichen Deklarationen in String.h und new.h. Um die Typ-beschreibung korrekt zu initialisieren, wird auch die private Definitionsdatei new.reingefügt, in der die Repräsentierung von struct Class vereinbart wird, die wir imAbschnitt 2.2 gesehen haben.

2.6 Noch eine Implementierung — AtomZur Illustration, was wir mit Konstruktor und Destruktor tatsächlich erreichen kön-nen, implementieren wir Atome. Ein Atom ist eine eindeutige Zeichenkette: Wennzwei Atome den gleichen Text enthalten, sind sie identisch. Atome kann man sehrbillig vergleichen: differ() ist genau dann wahr, wenn seine Argumentzeiger ver-schieden sind. Atome sind aufwendiger zu erzeugen und zu zerstören: Wir verwal-ten eine zirkuläre Liste aller Atome, und wir zählen, wie oft ein Atom kopiert wird:

struct String {const void * class; /* unbedingt zuerst */char * text;struct String * next;unsigned count;

};static struct String * ring; /* alle Atome */static void * String_clone (const void * _self){ struct String * self = (void *) _self;

++ self -> count;return self;

}Unsere zirkuläre Liste aller Atome wird in ring markiert, mit der Komponente

.next repräsentiert und vom Konstruktor und Destruktor unterhalten. Bevor derKonstruktor einen Text speichert, durchsucht er zunächst die Liste, ob der Text be-reits gespeichert wurde. Der folgende Code wird am Anfang von String_ctor() ein-gefügt:

if (ring){ struct String * p = ring;

doif (strcmp(p -> text, text) == 0){ ++ p -> count;

free(self);return p;

}while ((p = p -> next) != ring);

}

Page 30: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

20___________________________________________________________________________2 Dynamische Bindung — Generische Funktionen

elsering = self;

self -> next = ring -> next, ring -> next = self;self -> count = 1;

Wenn wir ein geeignetes Atom finden, vergrößern wir seinen Verweiszähler, gebendas neue String-Objekt self frei und liefern statt dessen das Atom p als Resultat.Andernfalls fügen wir das neue String-Objekt in die zirkuläre Liste ein und setzenseinen Verweiszähler auf 1.

Der Destruktor muß verhindern, daß ein Atom freigegeben wird, wenn sein Ver-weiszähler nicht auf Null reduziert wird. Der folgende Code wird am Anfang vonString_dtor() eingefügt:

if (-- self -> count > 0)return 0;

assert(ring);if (ring == self)

ring = self -> next;if (ring == self)

ring = 0;else{ struct String * p = ring;

while (p -> next != self){ p = p -> next;

assert(p != ring);}p -> next = self -> next;

}Wenn der verkleinerte Verweiszähler noch positiv ist, liefern wir einen Nullzeiger,damit delete() unser Objekt nicht freigibt. Andernfalls löschen wir den Anfang derzirkulären Liste in ring, wenn unser Atom das letzte ist, oder wir entfernen es ausder Liste.

Mit dieser Implementierung merkt unsere Applikation aus Abschnitt 2.4, daßdie kopierte Zeichenkette zum Original identisch ist und gibt etwa Folgendes aus:

sizeOf(a) == 16okclone?

2.7 ZusammenfassungWenn wir einen Zeiger auf ein Objekt haben, können wir mit Hilfe der dynamischenBindung typspezifische Funktionen finden: Jedes Objekt beginnt mit einer Be-schreibung, die Zeiger auf Funktionen enthält, die auf das Objekt angewendet wer-den können. Insbesondere enthält die Beschreibung Zeiger auf einen Konstruktorund einen Destruktor. Der Konstruktor initialisiert die Speicherfläche, die new() fürdas Objekt angelegt hat, und der Destruktor sorgt für Wiederverwendung der Res-sourcen im Objekt, bevor delete() die Speicherfläche selbst wieder freigibt.

Page 31: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

21___________________________________________________________________________2.8 Überlegungen

Wir nennen alle Objekte mit der gleichen Beschreibung eine Klasse. Ein Objektist eine Instanz einer Klasse, typspezifische Funktionen für ein Objekt nennt manMethoden, und Nachrichten sind die Aufrufe dieser Funktionen. Wir verwenden Se-lektor-Funktionen, um dynamisch gebundene Methoden für ein Objekt zu findenund aufzurufen.

Durch Selektoren und dynamische Bindung bewirkt der gleiche Funktionsnameverschiedene Effekte für verschiedene Klassen. Derartige Funktionen nennen wirpolymorph.

Polymorphe Funktionen sind sehr nützlich. Sie sorgen für Abstraktion bei derBedeutung: differ() vergleicht zwei beliebige Objekte — wir müssen nicht mehrwissen, welche Variation von differ() in einer konkreten Situation aufgerufen wer-den muß. Ein billiges und praktisches Werkzeug zur Fehlersuche ist eine polymor-phe Funktion store(), die ein beliebiges Objekt mit einem FILE-Zeiger ausgebenkann.

2.8 ÜberlegungenUm polymorphe Funktionen in Aktion zu sehen, müssen wir Object und Set mit dy-namischer Bindung implementieren. Bei Set ist das schwieriger, denn wir könnennicht mehr in den Elementen einer Menge notieren, zu welcher Menge sie gehören.

Wir brauchen eigentlich mehr Methoden für Zeichenketten: Wir sollten die Län-ge wissen, wir möchten neue Texte zuweisen, und wir sollten eine Zeichenketteausgeben können. Die Sache wird interessanter, wenn wir auch Teilketten betrach-ten.

Atome sind wesentlich effizienter, wenn wir sie mit einer Hash-Tabelle verwal-ten. Kann der Text eines Atoms geändert werden?

String_clone() wirft eine subtile Frage auf: In dieser Funktion sollte String dergleiche Wert sein wie self −> class. Macht es einen Unterschied, was wir an new()übergeben?

Page 32: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 33: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

23___________________________________________________________________________

3Programmierpraxis

Arithmetische Ausdrücke

Dynamische Bindung ist ganz allein eine sehr mächtige Programmiertechnik. AnStelle von wenigen Funktionen, die jeweils einen großen switch für viele Spezialfäl-le enthalten, können wir viele kleine Funktionen schreiben, die immer nur einen Fallbearbeiten müssen. Mit dynamischer Bindung sorgen wir dafür, daß die korrekteFunktion aufgerufen wird. Die Technik vereinfacht häufig Routineaufgaben, und sieresultiert normalerweise in Code, der sich sehr leicht erweitern läßt.

Als Beispiel schreiben wir ein kleines Programm, das arithmetische Ausdrückeliest und bewertet, die aus Gleitkommazahlen, Klammern und den üblichen Opera-toren für Addition, Subtraktion usw. bestehen. Normalerweise würden wir dieCompiler-Generatoren lex und yacc verwenden, um den Teil des Programms zu rea-lisieren, der einen arithmetischen Ausdruck erkennt, aber da dieses Buch nicht vomCompilerbau handelt, lösen wir auch dieses Problem ausnahmsweise von Hand. Inspäteren Kapiteln werden wir das Programm mehrfach erweitern.

3.1 Die HauptschleifeDie Hauptschleife des Programms liest eine Zeile aus der Standard-Eingabe, initiali-siert, so daß Zahlen und Operatoren extrahiert und Zwischenräume ignoriert wer-den können, ruft eine Funktion auf, die einen korrekten arithmetischen Ausdruck er-kennt und irgendwie speichert, und bewertet schließlich, was abgespeichert wurde.Wenn etwas schiefgeht, lesen wir einfach die nächste Eingabezeile. Hier ist dieHauptschleife:

#include <setjmp.h>

static enum tokens token; /* aktuelles Eingabesymbol */

static jmp_buf onError;

int main (void){ volatile int errors = 0;

char buf [BUFSIZ];

if (setjmp(onError))++ errors;

while (gets(buf))if (scan(buf)){ void * e = sum();

if (token)error("trash after sum");

process(e);delete(e);

}

return errors > 0;}

Page 34: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

24___________________________________________________________________________3 Programmierpraxis — Arithmetische Ausdrücke

void error (const char * fmt, ...){ va_list ap;

va_start(ap, fmt);vfprintf(stderr, fmt, ap), putc(’\n’, stderr);va_end(ap);longjmp(onError, 1);

}

Das Ziel eines Rücksprungs zur Fehlerbehandlung wird mit setjmp() festgelegt.Wenn error() irgendwo im Programm aufgerufen wird, setzt longjmp() die Aus-führung mit einem zweiten Resultat für setjmp() fort. Das Resultat ist hier derWert, der an longjmp() übergeben wurde. Der Fehler wird gezählt, und wir lesendie nächste Eingabezeile. Der exit-Code des Programms hängt davon ab, ob ir-gendwelche Fehler gefunden wurden.

3.2 Worte erkennenIn der Hauptschleife wird eine Eingabezeile in buf[] abgelegt und an scan() überge-ben. Diese Funktion hinterlegt bei jedem Aufruf das nächste Eingabesymbol in derVariablen token. Am Zeilenende hat token den Wert Null:

#include <ctype.h>#include <errno.h>#include <stdlib.h>

#include "parse.h" /* definiert NUMBER */

static double number; /* NUMBER: numerischer Wert */

static enum tokens scan (const char * buf)/* return token = naechstes Eingabesymbol */

{ static const char * bp;

if (buf)bp = buf; /* neue Eingabezeile */

while (isspace(* bp))++ bp;

if (isdigit(* bp) || * bp == ’.’){ errno = 0;

token = NUMBER, number = strtod(bp, (char **) & bp);if (errno == ERANGE)

error("bad value: %s", strerror(errno));}else

token = * bp ? * bp ++ : 0;return token;

}

Wir rufen scan() mit der Adresse einer Eingabezeile oder mit einem Nullzeiger auf;im letzteren Fall soll die aktuelle Zeile weiter zerlegt werden. Zwischenraum wirdignoriert, und bei einer führenden Ziffer oder einem Dezimalpunkt extrahieren wir ei-ne Gleitkommazahl mit der ANSI-C Funktion strtod(). Jedes andere Zeichen wird un-

Page 35: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

25___________________________________________________________________________3.3 Phrasen erkennen

verändert als Resultat geliefert, und wir gehen nicht am Null-Byte vorbei, das denEingabe-Puffer abschließt.

Das Resultat von scan() wird in der globalen Variablen token abgelegt — dasvereinfacht den Erkenner. Wenn wir eine Zahl entdeckt haben, liefern und hinterle-gen wir den eindeutigen Wert NUMBER, und wir speichern den Wert der Zahl in derglobalen Variablen number.

3.3 Phrasen erkennenAuf der obersten Ebene werden arithmetische Ausdrücke durch die Funktion sum()erkannt, die intern scan() aufruft und eine Repräsentierung des Ausdrucks liefert,die mit process() bewertet und durch delete() freigegeben werden kann.

Wenn wir yacc nicht benützen, erkennen wir arithmetische Ausdrücke durchdas Verfahren des rekursiven Abstiegs (recursive descent), bei dem Grammatikre-geln in äquivalente C-Funktionen übersetzt werden. Beispielsweise ist eine Summeein Produkt, dem beliebig viele Gruppen folgen, die jeweils aus einem Additionsope-rator und einem weiteren Produkt bestehen. Eine Grammatikregel wie

sum : product { +|- product }...

wird in folgende C-Funktion übersetzt:

void sum (void){

product();for (;;){ switch (token) {

case ’+’:case ’-’:

scan(0), product(); continue;}return;

}}

Für jede Grammatikregel gibt es eine C-Funktion, damit sich die Regeln als Funktio-nen gegenseitig aufrufen können. Alternativen werden in switch- oder if-Anweisungen übersetzt, Wiederholungen in der Grammatik ergeben Schleifen in C.Das einzige Problem besteht darin, daß wir unendliche Rekursion unterbinden müs-sen, deshalb wurde die Regel für sum mit einer Wiederholung und nicht links-rekur-siv formuliert.

token enthält jeweils das nächste Eingabesymbol. Wenn wir es erkennen,müssen wir scan(0) aufrufen, damit das nächste Symbol aus der Eingabe in tokenvorgehalten wird.

3.4 Ausdrücke verarbeitenWie verarbeiten wir einen arithmetischen Ausdruck? Wenn wir nur einfache Arith-metik mit numerischen Werten implementieren wollen, können wir die Erkenner-

Page 36: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

26___________________________________________________________________________3 Programmierpraxis — Arithmetische Ausdrücke

funktionen erweitern und Zwischenergebnisse berechnen, sobald wir die Operato-ren und Operanden erkennen: sum() sollte bei jedem Aufruf von product() eindouble-Resultat erhalten, um Addition oder Subtraktion so bald wie möglich durch-zuführen und das Resultat seinerseits als double-Wert zu liefern.

Wenn wir ein System konstruieren wollen, das kompliziertere Ausdrücke bear-beiten kann, müssen wir Ausdrücke speichern, um sie später zu bewerten. In die-sem Fall können wir nicht nur arithmetische Ausdrücke bearbeiten, sondern wirkönnen auch Bedingungen zulassen, um dann nur einen Teil eines Ausdrucks zu be-werten, und wir können gespeicherte Ausdrücke als Benutzerfunktionen innerhalbvon anderen Ausdrücken verwenden. Wir benötigen nur eine einigermaßen allge-meine Technik, um einen Ausdruck zu repräsentieren. Die konventionelle Lösungist ein binärer Baum, bei dem token in jedem Knoten gespeichert wird:

struct Node {enum tokens token;struct Node * left, * right;

};

Dies ist allerdings nicht gerade flexibel. Wir müssen mit union arbeiten, um einenKnoten zu konstruieren, in dem wir einen numerischen Wert speichern können, undwir verwenden unnötig viel Platz für Knoten, die unäre Operatoren repräsentieren.Außerdem enthalten process() und delete() dann switch-Anweisungen, die mit je-dem neuen Symbol wachsen, das wir erfinden.

3.5 Information verbergenWenn wir anwenden, was wir bisher gelernt haben, legen wir die Struktur einesKnotens überhaupt nicht offen. Statt dessen enthält eine Definitionsdatei value.hnur einige Deklarationen:

const void * Add;...

void * new (const void * type, ...);void process (const void * tree);void delete (void * tree);

Jetzt können wir sum() folgendermaßen codieren:

#include "value.h"

static void * sum (void){ void * result = product();

const void * type;

for (;;){ switch (token) {

case ’+’:type = Add;break;

case ’-’:type = Sub;break;

Page 37: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

27___________________________________________________________________________3.6 Dynamische Bindung

default:return result;

}scan(0);result = new(type, result, product());

}}

product() hat die gleiche Architektur wie sum() und ruft eine Funktion factor() auf,die Zahlen, Vorzeichen und eine Summe in Klammern erkennt:

static void * sum (void);

static void * factor (void){ void * result;

switch (token) {case ’+’:

scan(0);return factor();

case ’-’:scan(0);return new(Minus, factor());

default:error("bad factor: ’%c’ 0x%x", token, token);

case NUMBER:result = new(Value, number);break;

case ’(’:scan(0);result = sum();if (token != ’)’)

error("expecting )");}scan(0);return result;

}

Speziell in factor() müssen wir sehr vorsichtig sein, um die Invariante der Erken-nung aufrechtzuerhalten: token muß immer das nächste Eingabesymbol enthalten.Sobald token verbraucht wird, müssen wir scan(0) aufrufen.

3.6 Dynamische BindungDer Erkenner ist fertig. value.h verbirgt die Bewertung arithmetischer Ausdrückevollständig und gibt gleichzeitig vor, was wir implementieren müssen. new() erhälteine Beschreibung, wie Add, und geeignete Argumente, wie etwa Zeiger auf dieOperanden der Addition, und liefert einen Zeiger, der die Summe repräsentiert.

struct Type {void * (* new) (va_list ap);double (* exec) (const void * tree);void (* delete) (void * tree);

};

Page 38: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

28___________________________________________________________________________3 Programmierpraxis — Arithmetische Ausdrücke

void * new (const void * type, ...){ va_list ap;

void * result;

assert(type && ((struct Type *) type) -> new);

va_start(ap, type);result = ((struct Type *) type) -> new(ap);* (const struct Type **) result = type;va_end(ap);return result;

}

Wir verwenden dynamische Bindung und verweisen den Aufruf an eine knotenspe-zifische Funktion, die im Falle von Add den Knoten erzeugen und die zwei Zeigereintragen muß:

struct Bin {const void * type;void * left, * right;

};

static void * mkBin (va_list ap){ struct Bin * node = malloc(sizeof(struct Bin));

assert(node);node -> left = va_arg(ap, void *);node -> right = va_arg(ap, void *);return node;

}

Allein die Funktion mkBin() weiß, was für einen Knoten sie erzeugt. Wir verlangennur, daß die verschiedenen Knoten immer mit einem Zeiger für die dynamische Bin-dung beginnen. Dieser Zeiger wird von new() eingetragen, damit delete() seineknotenspezifische Funktion finden kann:

void delete (void * tree){

assert(tree && * (struct Type **) tree&& (* (struct Type **) tree) -> delete);

(* (struct Type **) tree) -> delete(tree);}

static void freeBin (void * tree){

delete(((struct Bin *) tree) -> left);delete(((struct Bin *) tree) -> right);free(tree);

}

Dynamische Bindung vermeidet sehr elegant, daß unsere Knoten kompliziertwerden. .new() erzeugt genau den richtigen Knoten für jede Typbeschreibung:Binäre Operatoren haben zwei Abkömmlinge, unäre Operatoren haben einen, undein Knoten für einen Wert enthält nur den Wert. delete() ist eine sehr einfacheFunktion, denn jeder Knoten kümmert sich um seine eigene Freigabe: Binäre Ope-

Page 39: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

29___________________________________________________________________________3.7 Postfix ausgeben

ratoren geben zwei Unterbäume und ihren eigenen Knoten frei, unäre Operatorenlöschen nur einen Unterbaum, und ein Knoten für einen Wert gibt nur sich selbstfrei. Variablen und Konstanten können sogar übrigbleiben — ihre Knoten würdenschlicht nichts tun, wenn sie von delete() angesprochen werden.

3.7 Postfix ausgebenBisher haben wir eigentlich nicht entschieden, was process() tun soll. Wenn wireinen Ausdruck in Postfix-Notation ausgeben wollen, also mit Operatoren nach ih-ren Operanden, fügen wir eine Zeichenkette zu struct Type hinzu, um den eigentli-chen Operator zu zeigen, und process() sorgt für eine einzige Ausgabezeile, die miteinem Tabulatorzeichen beginnt:

void process (const void * tree){

putchar(’\t’);exec(tree);putchar(’\n’);

}

exec() kümmert sich um die dynamische Bindung:

static void exec (const void * tree){

assert(tree && * (struct Type **) tree&& (* (struct Type **) tree) -> exec);

(* (struct Type **) tree) -> exec(tree);}

Jeder binäre Operator kann mit der folgenden Funktion ausgegeben werden:

static void doBin (const void * tree){

exec(((struct Bin *) tree) -> left);exec(((struct Bin *) tree) -> right);printf(" %s", (* (struct Type **) tree) -> name);

}

Die Typbeschreibungen binden dann alles zusammen:

static struct Type _Add = { "+", mkBin, doBin, freeBin };static struct Type _Sub = { "-", mkBin, doBin, freeBin };

const void * Add = & _Add;const void * Sub = & _Sub;

Wie ein numerischer Wert implementiert wird, ist leicht zu erraten. Der Wert wirdals Struktur mit einer double-Komponente für die Information repräsentiert:

struct Val {const void * type;double value;

};

Page 40: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

30___________________________________________________________________________3 Programmierpraxis — Arithmetische Ausdrücke

static void * mkVal (va_list ap){ struct Val * node = malloc(sizeof(struct Val));

assert(node);node -> value = va_arg(ap, double);return node;

}

Als Bearbeitung wird der Wert ausgegeben:

static void doVal (const void * tree){

printf(" %g", ((struct Val *) tree) -> value);}

Fertig — hier müssen wir keinen Unterbaum freigeben, folglich können wir die Bi-bliotheksfunktion free() direkt verwenden, um diesen Knoten freizugeben:

static struct Type _Value = { "", mkVal, doVal, free };

const void * Value = & _Value;

Ein unärer Operator wie Minus bleibt als Übungsaufgabe.

3.8 ArithmetikWenn wir einen arithmetischen Ausdruck nicht ausgeben, sondern bewerten wol-len, lassen wir jede exec-Funktion einen double-Wert liefern, den process() dannausgibt:

static double exec (const void * tree){

return (* (struct Type **) tree) -> exec(tree);}

void process (const void * tree){

printf("\t%g\n", exec(tree));}

Für jede Art von Knoten brauchen wir eine Funktion, die den Wert des Knotens be-rechnet und als Resultat liefert. Hier sind zwei Beispiele:

static double doVal (const void * tree){

return ((struct Val *) tree) -> value;}

static double doAdd (const void * tree){

return exec(((struct Bin *) tree) -> left) +exec(((struct Bin *) tree) -> right);

}

static struct Type _Add = { mkBin, doAdd, freeBin };static struct Type _Value = { mkVal, doVal, free };

const void * Add = & _Add;const void * Value = & _Value;

Page 41: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

31___________________________________________________________________________3.9 Infix ausgeben

3.9 Infix ausgebenDie Hohe Schule in der Behandlung arithmetischer Ausdrücke besteht vielleicht dar-in, sie mit möglichst wenigen Klammern auszugeben. Das ist normalerweise einbißchen diffizil, je nachdem, wer für die Ausgabe der Klammern verantwortlich ist.Zusätzlich zu dem Operatornamen, den wir schon für die Postfix-Ausgabe ein-geführt haben, fügen wir noch zwei Zahlen zu struct Type hinzu:

struct Type {const char * name; /* Name des Knotens */char rank, rpar;void * (* new) (va_list ap);void (* exec) (const void * tree, int rank, int par);void (* delete) (void * tree);

};

.rank ist der Vorrang des Operators, wobei wir mit 1 für Addition beginnen. .rparist von Null verschieden für Operationen wie Subtraktion, bei denen der rechte Ope-rand eingeklammert werden muß, wenn dieser Operand einen Operator mit glei-chem Vorrang verwendet. Als Beispiel betrachten wir

$ infix1 + (2 - 3)

1 + 2 - 31 - (2 - 3)

1 - (2 - 3)

Dies zeigt, daß wir folgende Initialisierung benötigen:

static struct Type _Add = {"+", 1, 0, mkBin, doBin, freeBin};static struct Type _Sub = {"-", 1, 1, mkBin, doBin, freeBin};

Das eigentliche Problem besteht darin, wie ein binärer Knoten entscheidet, ober sich mit Klammern umgeben soll. Ein binärer Knoten, wie etwa eine Addition, er-hält den Vorrang seines Vorgängers und einen Hinweis, ob bei gleichem VorrangKlammern benötigt werden. doBin() entscheidet, ob Klammern benutzt werden:

static void doBin (const void * tree, int rank, int par){ const struct Type * type = * (struct Type **) tree;

par = type -> rank < rank|| (par && type -> rank == rank);

if (par) putchar(’(’);

Wenn unser Knoten weniger Vorrang hat als sein Vorgänger, oder wenn wir bei glei-chem Vorrang Klammern verwenden sollen, geben wir Klammern aus. In jedemFall, wenn in unserer Beschreibung .rpar gesetzt ist, verlangen wir nur von unse-rem rechten Operanden, daß er zusätzliche Klammern ausgibt:

exec(((struct Bin *) tree) -> left, type -> rank, 0);printf(" %s ", type -> name);exec(((struct Bin *) tree) -> right,

type -> rank, type -> rpar);if (par) putchar(’)’);

}

Page 42: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

32___________________________________________________________________________3 Programmierpraxis — Arithmetische Ausdrücke

Die restlichen Ausgabefunktionen sind erheblich leichter zu schreiben.

3.10 ZusammenfassungDrei verschiedene Prozessoren demonstrieren die Vorteile, wenn wir Informationkonsequent verbergen. Dynamische Bindung hat sehr geholfen, ein Problem in vie-le einfache Funktionen zu zerlegen. Das resultierende Programm kann sehr leichterweitert werden — wir können ohne weiteres Vergleiche oder einen C-Operatorwie ? : hinzufügen.

Page 43: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

33___________________________________________________________________________

4Vererbung

Code wiederverwenden und anpassen

4.1 Eine Oberklasse — PointIn diesem Kapitel beginnen wir ein rudimentäres Zeichenprogramm. Hier ist ein ein-facher Test für eine der Klassen, die wir gerne hätten:

#include "Point.h"#include "new.h"

int main (int argc, char ** argv){ void * p;

while (* ++ argv){ switch (** argv) {

case ’p’:p = new(Point, 1, 2);break;

default:continue;

}draw(p);move(p, 10, 20);draw(p);delete(p);

}return 0;

}

Für jedes Kommandoargument, das mit dem Buchstaben p beginnt, erhalten wireinen neuen Punkt, der gezeichnet, verschoben, nochmals gezeichnet und gelöschtwird. ANSI-C enthält keine Standardfunktionen für grafische Ausgabe. Wenn wirtrotzdem unbedingt ein Bild produzieren wollen, können wir Text ausgeben, denKernighans pic [Ker82] versteht:

$ points p"." at 1,2"." at 11,22

Für den Test spielen die Koordinaten keine Rolle.

Was können wir mit einem Punkt tun? new() erzeugt einen Punkt, und derKonstruktor erwartet die Anfangskoordinaten als weitere Argumente für new(). Wieüblich gibt delete() unseren Punkt frei, und nach Konvention führen wir auch einenDestruktor ein.

draw() sorgt dafür, daß der Punkt gezeichnet wird. Der switch im Testpro-gramm deutet an, daß wir noch mit anderen grafischen Objekten arbeiten wollen.Deshalb geben wir draw() dynamische Bindung.

Page 44: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

34___________________________________________________________________________4 Vererbung — Code wiederverwenden und anpassen

move() verschiebt einen Punkt, indem die Argumente zu den Koordinaten ad-diert werden. Wenn wir jedes grafische Objekt relativ zu einem eigenen Referenz-punkt implementieren, können wir es dadurch verschieben, daß wir einfach move()auf den Referenzpunkt anwenden. Wir sollten deshalb ohne dynamische Bindungfür move() auskommen.

4.2 Implementierung der Oberklasse — PointDie Vereinbarung des abstrakten Datentyps in Point.h enthält folgendes:

extern const void * Point; /* new(Point, x, y); */

void move (void * point, int dx, int dy);

Wir können alle Dateien new.? aus dem zweiten Kapitel übernehmen, wobei wir diemeisten Methoden entfernen und draw() in new.h hinzufügen:

void * new (const void * class, ...);void delete (void * item);void draw (const void * self);

Die Typbeschreibung struct Class in new.r sollte mit der Deklaration der Methodedraw() in new.h übereinstimmen:

struct Class {size_t size;void * (* ctor) (void * self, va_list * app);void * (* dtor) (void * self);void (* draw) (const void * self);

};

Der Selektor draw() wird in new.c implementiert. Er ersetzt Selektoren wiediffer(), die im Abschnitt 2.3 eingeführt wurden, und wird analog codiert:

void draw (const void * self){ const struct Class * const * cp = self;

assert(self && * cp && (* cp) -> draw);(* cp) -> draw(self);

}

Nach diesen Vorbereitungen können wir uns mit der wirklichen Arbeit beschäfti-gen und Point.c schreiben, die Implementierung von Punkten. Wieder hat die Ob-jekt-Orientierung dafür gesorgt, daß wir genau wissen, was wir tun müssen: Wirmüssen eine Repräsentierung festlegen und einen Konstruktor, einen Destruktorsowie die dynamisch gebundene Methode draw() und die statisch gebundene Me-thode move(), also eine einfache Funktion, implementieren. Wenn wir uns fürzweidimensionale, kartesische Koordinaten entscheiden, verwenden wir die offen-sichtliche Repräsentierung:

struct Point {const void * class;int x, y; /* Koordinaten */

};

Page 45: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

35___________________________________________________________________________4.3 Vererbung — ‘‘Circle’’

Der Konstruktor muß die Koordinaten .x und .y initialisieren — inzwischen reineRoutine:

static void * Point_ctor (void * _self, va_list * app){ struct Point * self = _self;

self -> x = va_arg(* app, int);self -> y = va_arg(* app, int);return self;

}

Es stellt sich heraus, daß wir keinen Destruktor benötigen, denn wir müssen keineRessourcen retten, bevor delete() selbst struct Point freigibt. In Point_draw() ge-ben wir die aktuellen Koordinaten so aus, daß sie pic verstehen kann:

static void Point_draw (const void * _self){ const struct Point * self = _self;

printf("\".\" at %d,%d\n", self -> x, self -> y);}

Damit existieren alle dynamisch gebundenen Methoden, und wir können die Typbe-schreibung definieren, in der ein Nullzeiger den fehlenden Destruktor vertritt:

static const struct Class _Point = {sizeof(struct Point), Point_ctor, 0, Point_draw

};

const void * Point = & _Point;

move() ist nicht dynamisch gebunden, deshalb vermeiden wir static und exportie-ren die Funktion aus Point.c; der Name bleibt ebenfalls unverändert:

void move (void * _self, int dx, int dy){ struct Point * self = _self;

self -> x += dx, self -> y += dy;}

Damit sind Punkte in Point.? und die dynamische Bindung in new.? fertig implemen-tiert.

4.3 Vererbung — CircleEin Kreis ist eigentlich nur ein großer Punkt: Zusätzlich zu den Mittelpunkt-Koordina-ten braucht er einen Radius. Gezeichnet wird ein bißchen anders, aber zum Ver-schieben müssen wir nur die Koordinaten des Mittelpunkts ändern.

Normalerweise werfen wir jetzt den Texteditor an und sorgen für Wiederver-wendung im Quelltext. Wir kopieren die Implementierung der Punkte und änderndie Teile ab, wo sich ein Kreis anders verhält als ein Punkt. struct Circle erhält einezusätzliche Komponente:

int rad;

Diese Komponente wird im Konstruktor initialisiert

self -> rad = va_arg(* app, int);

Page 46: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

36___________________________________________________________________________4 Vererbung — Code wiederverwenden und anpassen

und in Circle_draw() benützt:

printf("circle at %d,%d rad %d\n",self -> x, self -> y, self -> rad);

Es klemmt ein bißchen in move(). Hier müssen wir für Punkt und Kreis dassel-be tun: Wir addieren die Funktionsargumente zu den Koordinaten, damit der Punktbeziehungsweise Mittelpunkt verschoben wird. Im einen Fall muß move() jedochdie Koordinaten aus struct Point und im andern Fall aus struct Circle verwenden.Wenn move() dynamisch gebunden wäre, könnten wir zwei verschiedene Funktio-nen schreiben, die dasselbe tun, aber es geht viel einfacher. Betrachten wir dazudie Layouts der Repräsentierungen für Punkt und Kreis:

Punkt

class

x

y

struct Point

Kreis

class

x

y

rad

struct Circle

.......................

Das Bild zeigt, daß jeder Kreis mit einem Punkt beginnt. Wenn wir struct Circle da-durch erzeugen, daß wir den Radius am Ende von struct Point hinzufügen, könnenwir einfach einen Kreis an move() übergeben, denn der Anfang seiner Repräsentie-rung sieht genauso aus wie ein Punkt, den move() eigentlich erwartet, und denmove() als einziges ändern kann. Mit der folgenden Deklaration können wir garan-tieren, daß ein Kreis immer auch wie ein Punkt aussieht:

struct Circle { const struct Point _; int rad; };

Wir lassen die neue, abgeleitete Struktur immer mit einer Kopie der Basisstrukturbeginnen, die wir verlängern. Wir wollen Information verbergen und deshalb nie di-rekt in die Basisstruktur hineingreifen, folglich verwenden wir einen fast unsichtba-ren Unterstrich als Komponentennamen und deklarieren die Komponente const, umallzu tapfere Zuweisungen zu unterbinden.

Damit haben wir schon einfache Vererbung: Eine Unterklasse wird von einerOberklasse (oder Basisklasse ) abgeleitet, indem man einfach die Struktur verlän-gert, die ein Objekt der Oberklasse repräsentiert.

Da die Repräsentierung eines Unterklassen-Objekts (Kreis) genauso anfängt wiedie Repräsentierung eines Oberklassen-Objekts (Punkt), kann ein Kreis immer be-haupten, er sei ein Punkt — bei der Anfangsadresse der Repräsentierung des Krei-ses befindet sich wirklich die Repräsentierung eines Punkts.

Wir codieren völlig sicher und korrekt, wenn wir jetzt einen Kreis an move()übergeben: Die Unterklasse erbt die Methoden der Oberklasse, denn diese Metho-den bearbeiten nur den Teil der Unterklassen-Repräsentierung, der identisch zurOberklassen-Repräsentierung ist, für die die Methoden ursprünglich entwickelt wur-

Page 47: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

37___________________________________________________________________________4.4 Bindung und Vererbung

den. Wenn wir einen Kreis als Punkt übergeben, wandeln wir eigentlich struct Cir-cle * in struct Point * um. Wir bezeichnen dies als up-cast , eine Aufwärts-Um-wandlung von einer Unterklasse in eine Oberklasse — in ANSI-C benötigen wir dazueinen expliziten Umwandlungsoperator, oder wir müssen void * dazwischenschal-ten.

In der umgekehrten Richtung gibt es aber meistens Probleme, wenn wir nureinen Punkt an eine Funktion wie Circle_draw() übergeben, die mit einem Kreisrechnet: Eine Umwandlung von struct Point * zu struct Circle * ist nur dann zuläs-sig, wenn der Punkt ursprünglich schon ein Kreis war. Wir nennen dies down-cast ,eine Abwärts-Umwandlung von einer Oberklasse in eine Unterklasse — auch hierbenötigen wir eine explizite Umwandlungsoperation oder void *, und wir dürfendies nur mit Zeigern auf solche Objekte veranstalten, die von Anfang an zur Unter-klasse gehört haben.

4.4 Bindung und VererbungDie Funktion move() ist nicht dynamisch gebunden und verwendet auch keine dy-namisch gebundene Methode für ihre Arbeit. Wir können zwar Punkte wie Kreisean move() übergeben, aber es ist trotzdem keine richtig polymorphe Funktion:move() benimmt sich nicht verschieden für verschiedene Objekttypen; move() ad-diert immer nur Argumente zu Koordinaten, unabhängig davon, was vielleicht anden Koordinaten hängt.

Die Situation ist ganz anders für eine dynamisch gebundene Methode wiedraw(). Betrachten wir das vorherige Bild nochmals, diesmal jedoch mit den Typbe-schreibungen dazu:

Punkt

x

y

struct Point

Point

size

ctor

dtor

draw

struct Class

Kreis

x

y

rad

struct Circle

.......................

Circle

size

ctor

dtor

draw

struct Class

Wenn wir von einem Kreis in einen Punkt aufwärts umwandeln, ändern wir den Zu-stand des Kreises nicht, das heißt, wenn wir auch struct Circle, die Repräsentie-rung des Kreises, so betrachten, als ob sie struct Point wäre, ändern wir ihren In-halt nicht. Folglich hat der als Punkt betrachtete Kreis noch immer Circle als Typbe-schreibung, denn der Zeiger in seiner .class-Komponente hat sich nicht geändert.draw() ist eine Selektor-Funktion, das heißt, sie verwendet einfach das Argument,das als self übergeben wird, geht von dort zur Typbeschreibung in .class und ruftdie Zeichenmethode auf, die dort gespeichert ist.

Page 48: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

38___________________________________________________________________________4 Vererbung — Code wiederverwenden und anpassen

Eine Unterklasse erbt die statisch gebundenen Methoden ihrer Oberklasse —diese Methoden bearbeiten den Teil des Unterklassen-Objekts, der auch schon imOberklassen-Objekt vorhanden ist. Eine Unterklasse kann jedoch ihre eigenen Me-thoden an Stelle der dynamisch gebundenen Methoden ihrer Oberklasse einführen.Wenn sie die dynamisch gebundenen Methoden der Oberklasse erbt und nicht er-setzt, benehmen sich die geerbten, dynamisch gebundenen Methoden der Ober-klasse so wie statisch gebundene Methoden, das heißt, sie ändern den Oberklas-sen-Teil eines Unterklassen-Objekts. Wenn eine dynamisch gebundene Methode inder Unterklasse aber ersetzt wird, hat die Unterklassen-Methode natürlich Zugriffauf die gesamte Repräsentierung des Unterklassen-Objekts, das heißt, für einenKreis ruft draw() dann Circle_draw() auf, und diese Unterklassen-Methode kannden Radius berücksichtigen, wenn der Kreis gezeichnet wird.

4.5 Statische und dynamische BindungEine Unterklasse erbt die statisch gebundenen Methoden ihrer Oberklasse undkann wählen, ob sie die dynamisch gebundenen Methoden erben oder ersetzenwill. Betrachtet man die Deklarationen von move() und draw()

void move (void * point, int dx, int dy);

void draw (const void * self);

stellt man fest, daß man die Art der Bindung aus den Deklarationen nicht ersieht,obgleich ja move() direkt arbeitet und draw() nur eine Selektor-Funktion ist, die erstzur Laufzeit die dynamische Bindung auswertet. Der einzige Unterschied bestehtdarin, daß wir eine statisch gebundene Methode wie move() als Teil der Deklarationdes abstrakten Datentyps in Point.h vereinbaren und eine dynamisch gebundeneMethode wie draw() zusammen mit der Speicherverwaltung in new.h, denn wir ha-ben bisher beschlossen, die Selektor-Funktionen in new.c zu implementieren.

Statische Bindung ist effizienter, denn der C-Compiler kann für einen Unterpro-grammaufruf einen Sprungbefehl mit einer direkten Adresse codieren, aber eineFunktion wie move() kann in einer Unterklasse nicht ersetzt werden. DynamischeBindung ist flexibler, kostet aber einen indirekten Aufruf — wir haben sogar be-schlossen, eine Selektor-Funktion wie draw() einzuschalten, um die Argumente zuprüfen und die richtige Methode zu finden und aufzurufen. Wir könnten auf die Prü-fung und den zusätzlichen Funktionsaufruf verzichten, indem wir einen Makro* ver-wenden, zum Beispiel:

#define draw(self) \((* (struct Class **) self) -> draw (self))

Makros sind jedoch problematisch, wenn ihre Argumente Nebeneffekte wie Zuwei-sungen oder Inkremente verursachen, und es gibt keine vernünftige Lösung für va-riable Parameterlisten bei Makros. Außerdem benötigt dieser Makro die Deklarationvon struct Class, die wir bisher nur in der Implementierung, aber nicht bei der Be-nutzung von Klassen offengelegt haben.____________________________________________________________________________________________

* In ANSI-C werden Makros nicht rekursiv expandiert, so daß ein Makro eine Funktion mit dem gleichenNamen verbergen kann.

Page 49: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

39___________________________________________________________________________4.6 Sichtbarkeit und Zugriffsfunktionen

Nach welchen Kriterien entscheidet man, ob eine Methode besser statisch oderdynamisch gebunden sein soll? Leider legen wir so ziemlich alles bereits beim Ent-wurf der Oberklasse fest. Wenn wir eine Methode von statischer auf dynamischeBindung oder umgekehrt umstellen wollen, ändern sich zwar die Aufrufe der Me-thode nicht, aber wir müssen sehr viel Text, vielleicht sogar in vielen Klassen, editie-ren. Ab Kapitel 7 werden wir einen einfachen Präprozessor einsetzen, um die Co-dierung zu vereinfachen, aber auch dann ist der Umbau der Bindung fehleranfällig.

Im Zweifelsfall sollten wir uns wahrscheinlich eher für dynamische Bindung ent-scheiden, auch wenn sie weniger effizient ist. Generische Funktionen führen oft zueiner nützlichen, konzeptionellen Abstraktion, und sie können die Anzahl der Funkti-onsnamen deutlich reduzieren, an die wir uns im Rahmen eines Projekts erinnernmüssen. Sollten wir nach Implementierung aller Klassen entdecken, daß eine dyna-misch gebundene Methode nie ersetzt wurde, ist es wesentlich einfacher, ihren Se-lektor durch die einzige Implementierung zu ersetzen und sogar ihre Komponente instruct Class zu verschwenden, als die Typbeschreibung erweitern und alle Initiali-sierungen korrigieren zu müssen.

4.6 Sichtbarkeit und ZugriffsfunktionenWir können jetzt versuchen, Circle_draw() zu implementieren. Wir sollten interneInformationen nach Möglichkeit verbergen, also verwenden wir drei Dateien proKlasse, die je nach Notwendigkeit offengelegt werden. In der SchnittstellendateiCircle.h vereinbaren wir den abstrakten Datentyp; bei einer Unterklasse müssen wirdie Schnittstellendatei der Oberklasse einfügen, um die Deklarationen der eingeerb-ten Methoden bereitzustellen:

#include "Point.h"

extern const void * Circle; /* new(Circle, x, y, rad) */

Die Schnittstellendatei Circle.h wird sowohl von der Anwendung als auch bei derImplementierung der Klasse eingefügt; sie wird wie üblich gegen mehrfaches Einfü-gen geschützt.

Die Repräsentierung eines Kreises wird in einer zweiten Definitionsdatei Circle.rfestgelegt. Bei einer Unterklasse müssen wir die Repräsentierungsdatei der Ober-klasse einfügen, damit wir die Repräsentierung der Unterklasse mit Hilfe der Ober-klasse beschreiben können:

#include "Point.r"

struct Circle { const struct Point _; int rad; };

Die Unterklasse braucht die Repräsentierung der Oberklasse, um Vererbung zu im-plementieren: struct Circle enthält const struct Point. Der Punkt ist bestimmtnicht konstant — move() ändert seine Koordinaten — aber das const-Attributschützt davor, daß die Komponenten im Rahmen von Circle versehentlich über-schrieben werden. Die Repräsentierungsdatei Circle.r wird nur bei der Implemen-tierung der Klasse eingefügt; auch sie ist gegen mehrfaches Einfügen geschützt.

Page 50: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

40___________________________________________________________________________4 Vererbung — Code wiederverwenden und anpassen

Zum Schluß definieren wir die Implementierung eines Kreises in der QuelldateiCircle.c, wobei die Schnittstellendatei und Repräsentierungsdatei der Klasse sowie(noch) die Deklaration der Speicherverwaltung eingefügt werden:

#include "Circle.h"#include "Circle.r"#include "new.h"#include "new.r"

static void Circle_draw (const void * _self){ const struct Circle * self = _self;

printf("circle at %d,%d rad %d\n",self -> _.x, self -> _.y, self -> rad);

}

In Circle_draw() haben wir die Komponenten des Kreismittelpunkts gelesen, indemwir in den Oberklassen-Bereich über den ‘‘unsichtbaren’’ Namen _ eingedrungensind. Wenn wir Information verborgen halten wollen, ist das keine sehr gute Idee.Zwar entstehen durch das Lesen von Koordinaten sicher keine größeren Probleme,aber wir müssen natürlich immer befürchten, daß in anderen Situationen eine Un-terklassen-Implementierung mogelt und ihren Oberklassen-Bereich direkt verändertund damit die Invarianten möglicherweise total durcheinanderbringt.

Effizienz verlangt, daß eine Unterklasse in ihre Oberklassen-Komponenten di-rekt eingreift. Informationsfluß- und Pflegeüberlegungen diktieren, daß eine Ober-klasse ihre eigene Repräsentierung möglichst gut vor ihren Unterklassen versteckt.Wenn wir den letzteren Aspekt für wichtiger halten, sollten wir Zugriffsfunktionenfür alle Komponenten einer Oberklasse bereitstellen, die eine Unterklasse ansehendarf, und Zuweisungsfunktionen für die Komponenten, die die Unterklasse änderndarf — wenn überhaupt.

Zugriffs- und Zuweisungsfunktionen sind statisch gebundenen Methoden.Wenn wir sie in der Repräsentierungsdatei der Oberklasse deklarieren, die nur beider Implementierung von Unterklassen eingefügt wird, können wir Makros verwen-den, denn Nebeneffekte sind kein Problem, wenn ein Makro jedes Argument nureinmal verwendet. Als Beispiel definieren wir in Point.r folgende Zugriffsmakros:*

#define x(p) (((const struct Point *)(p)) -> x)#define y(p) (((const struct Point *)(p)) -> y)

Diese Makros können auf jeden Zeiger angewendet werden, der auf ein Objekt ver-weist, das mit struct Point beginnt, das heißt, auf Objekte aus jeder Unterklasseunserer Punkte. Die Technik besteht darin, den Zeiger aufwärts in unsere Oberklas-se umzuwandeln und dort auf die interessante Komponente zu verweisen. constist Teil der Umwandlung, um Zuweisungen an das Resultat zu unterbinden. Wennconst nicht angegeben ist,

#define x(p) (((struct Point *)(p)) -> x)

____________________________________________________________________________________________

* In ANSI-C wird ein Makro mit Parametern nur expandiert, wenn der Makroname vor einer linken Klam-mer steht. Überall sonst verhält sich der Makroname wie jedes andere Symbol.

Page 51: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

41___________________________________________________________________________4.7 Implementierung der Unterklasse — ‘‘Circle’’

dann würde ein Makroaufruf x(p) einen L-Wert produzieren, der auch das Ziel einerZuweisung sein kann. Eine zweckmäßigere Zuweisungsfunktion wäre die folgendeMakrodefinition:

#define set_x(p,v) (((struct Point *)(p)) -> x = (v))

die eine Zuweisung konstruiert.

Außerhalb der Implementierung einer Unterklasse können wir nur statisch ge-bundene Methoden für Zugriffs- und Zuweisungsfunktionen verwenden. Wir kön-nen keine Makros einsetzen, da die Repräsentierung der Oberklasse dann für Ver-weise innerhalb der Makros nicht zur Verfügung steht. Wir verbergen die Informati-on, indem wir die Repräsentierungsdatei Point.r nicht zum Einfügen in eine Anwen-dung zur Verfügung stellen.

Die Makrodefinitionen demonstrieren, daß wir Information nicht mehr erfolg-reich verbergen können, sobald wir die Repräsentierung einer Klasse offenlegen, al-so auch im Verhältnis Unterklasse zu Oberklasse. Wir können natürlich struct Pointnoch wesentlich besser verstecken. Zur Implementierung der Oberklasse verwen-den wir die normale Vereinbarung:

struct Point {const void * class;int x, y; /* Koordinaten */

};

Zur Implementierung von Unterklassen stellen wir nur die folgende opake Versionzur Verfügung:

struct Point {const char _ [ sizeof( struct {

const void * class;int x, y; /* Koordinaten */

})];};

Die Struktur hat zwar die gleiche Größe wie vorher, aber wir können ihre Kompo-nenten weder lesen noch schreiben, da sie in einer anonymen inneren Struktur ver-steckt sind. Beide Deklarationen müssen natürlich identische Komponentendeklara-tionen enthalten, und das ist ohne einen Präprozessor sehr schwer durchzuhalten.

4.7 Implementierung der Unterklasse — CircleWir können jetzt Kreise vollständig implementieren und dabei die Techniken aus denvorhergehenden Abschnitten wählen, die uns am besten gefallen. Die Objekt-Ori-entierung schreibt vor, daß wir einen Konstruktor und vielleicht einen Destruktorentwickeln sowie Circle_draw() und eine Typbeschreibung Circle, um alles zu ver-binden. Damit wir unsere Methoden ausprobieren können, holen wir im Testpro-gramm aus Abschnitt 4.1 die Definitionsdatei Circle.h hinzu und erweitern denswitch um folgende Zeilen:

case ’c’:p = new(Circle, 1, 2, 3);break;

Page 52: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

42___________________________________________________________________________4 Vererbung — Code wiederverwenden und anpassen

Jetzt können wir unser Testprogramm beispielsweise so aufrufen:

$ circles p c"." at 1,2"." at 11,22circle at 1,2 rad 3circle at 11,22 rad 3

Der Konstruktor für Circle erhält drei Argumente: zuerst die Koordinaten desMittelpunkts und dann den Radius. Den Point-Bereich muß der Point-Konstruktorinitialisieren. Er verwendet dazu einen Teil der Argumentliste von new(). DerCircle-Konstruktor erhält den Rest der Argumentliste und initialisiert damit den Radi-us.

Ein Unterklassen-Konstruktor sollte zuerst den Oberklassen-Konstruktor den Teilder Initialisierung vornehmen lassen, der einen leeren Speicherbereich in ein Ober-klassen-Objekt verwandelt. Wenn der Oberklassen-Konstruktor damit fertig ist, ver-vollständigt der Unterklassen-Konstruktor die Initialisierung und verwandelt dasOberklassen- in ein Unterklassen-Objekt.

Für Kreise bedeutet das, daß wir intern Point_ctor() aufrufen müssen. Wie alledynamisch gebundenen Methoden ist auch diese Funktion static in Point.c definiertund damit verborgen. Mit Hilfe der Typbeschreibung Point, die in Circle.c zur Verfü-gung steht, können wir die Funktion aber immer noch erreichen:

static void * Circle_ctor (void * _self, va_list * app){ struct Circle * self =

((const struct Class *) Point) -> ctor(_self, app);

self -> rad = va_arg(* app, int);return self;

}

Jetzt sollte klar sein, warum wir die Adresse app des Argumentlistenzeigers an je-den Konstruktor übergeben und nicht etwa den va_list-Wert selbst: new() ruft denUnterklassen-Konstruktor auf, der dann den Oberklassen-Konstruktor aufruft, usw.Der oberste Konstruktor ist dann der erste, der wirklich etwas tut, und er hat als er-ster Zugriff auf den Anfang der Argumentliste, die ursprünglich an new() übergebenwurde. Die restlichen Argumente stehen für die erste Unterklasse zur Verfügungund so weiter, bis die letzten Argumente, am rechten Ende der Liste, von der letz-ten Unterklasse benützt werden, das heißt, von dem Konstruktor, den new() direktaufgerufen hat.

Die Destruktoren laufen am besten genau umgekehrt ab: delete() ruft den Un-terklassen-Destruktor auf. Er sollte seine eigenen Ressourcen freigeben und dannseinen direkten Oberklassen-Destruktor aufrufen, der den nächsten Satz Ressour-cen einsammelt, und so weiter. Die Konstruktion erfolgt von der Oberklasse zurUnterklasse, die Destruktion umgekehrt, Unterklasse vor Oberklasse, Kreis-Teil vorPunkt-Teil. Im vorliegenden Fall müssen wir allerdings ohnehin nichts tun.

Möglichkeiten zur Implementierung von Circle_draw() haben wir schon im Ab-schnitt 4.6 besprochen. In der offiziellen Version verwenden wir sichtbare Kompo-nenten und codieren die Repräsentierungsdatei Point.r folgendermaßen:

Page 53: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

43___________________________________________________________________________4.8 Zusammenfassung

struct Point {const void * class;int x, y; /* Koordinaten */

};

#define x(p) (((const struct Point *)(p)) -> x)#define y(p) (((const struct Point *)(p)) -> y)

Jetzt können wir diese Zugriffsmakros für Circle_draw() benützen:

static void Circle_draw (const void * _self){ const struct Circle * self = _self;

printf("circle at %d,%d rad %d\n",x(self), y(self), self -> rad);

}

move() hat statische Bindung und wird aus der Implementierung der Punktevererbt. Wir vervollständigen die Implementierung der Kreise, indem wir die Typbe-schreibung definieren, die als einziges in Circle.c global sichtbar ist:

static const struct Class _Circle = {sizeof(struct Circle), Circle_ctor, 0, Circle_draw

};

const void * Circle = & _Circle;

Es sieht zwar so aus, als ob wir eine praktikable Strategie entwickelt haben,wie wir den Programmtext zur Implementierung einer Klasse auf die Schnittstellen-,Repräsentierungs- und Quelldatei verteilen, aber das Beispiel mit Punkten und Krei-sen hat eine Problematik nicht demonstriert: Wenn eine dynamisch gebundeneMethode wie Point_draw() in der Unterklasse nicht ersetzt wird, muß die Typbe-schreibung der Unterklasse auf die Funktion zeigen, die in der Oberklasse definiertwurde. Leider ist der Funktionsname dort aber static definiert, so daß wir die Initia-lisierung nicht ohne weiteres codieren können. Im sechsten Kapitel werden wir ei-ne einwandfreie Lösung für dieses Problem kennenlernen. Inzwischen müssen wirin diesem Fall eben auf static verzichten, den Funktionskopf nochmals bei der Im-plementierung der Unterklasse deklarieren und den Funktionsnamen zur Initialisie-rung der Typbeschreibung der Unterklasse verwenden.

4.8 ZusammenfassungDie Objekte einer Ober- und einer Unterklasse verhalten sich ähnlich, aber nichtgleich. Unterklassen-Objekte haben normalerweise einen komplexeren Zustandund mehr Methoden — sie sind spezialisierte Versionen der Oberklassen-Objekte.

Wir beginnen die Repräsentierung eines Unterklassen-Objekts mit einer Kopieder Repräsentierung eines Oberklassen-Objekts, das heißt, ein Unterklassen-Objektwird repräsentiert, indem wir Komponenten am Ende eines Oberklassen-Objektsanfügen.

Eine Unterklasse erbt die Methoden der Oberklasse: Da der Anfang eines Un-terklassen-Objekts genauso wie ein Oberklassen-Objekt aussieht, können wir im-mer aufwärts umwandeln und einen Zeiger auf ein Unterklassen-Objekt als Zeiger

Page 54: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

44___________________________________________________________________________4 Vererbung — Code wiederverwenden und anpassen

auf ein Oberklassen-Objekt auffassen, den wir dann an eine Oberklassen-Methodeübergeben dürfen. Um explizite Umwandlungen zu vermeiden, deklarieren wir alleMethoden-Parameter mit void * als generische Zeiger.

Vererbung ist eine rudimentäre Form der Polymorphie: Eine Oberklassen-Me-thode akzeptiert Objekte mit verschiedenen Typen, nämlich Objekte aus seiner ei-genen Klasse und aus allen Unterklassen. Da die Objekte aber alle so tun, als obsie Oberklassen-Objekte sind, bearbeitet die Methode immer nur den Oberklassen-Teil eines Objekts, und sie würde folglich Objekte verschiedener Klassen nicht ver-schieden behandeln.

Dynamisch gebundene Methoden kann man von einer Oberklasse erben oder ineiner Unterklasse ersetzen — dies hängt davon ab, welche Zeiger man in der Unter-klasse in die Typbeschreibung einträgt. Wenn also eine dynamisch gebundene Me-thode für ein Objekt aufgerufen wird, können wir immer die Methode erreichen, diezur wirklichen Klasse des Objekts gehört, auch wenn der Zeiger auf das Objekt viel-leicht irgendwie aufwärts umgewandelt wurde. Wenn eine dynamisch gebundeneMethode eingeerbt wird, kann sie nur den Oberklassen-Teil des Unterklassen-Ob-jekts bearbeiten, denn sie weiß nichts von der Existenz der Unterklasse. Wenn ei-ne Methode ersetzt wird, kann die Unterklassen-Version das ganze Objekt bearbei-ten und sogar die entsprechende Oberklassen-Version der Methode mit Hilfe derTypbeschreibung der Oberklasse explizit aufrufen.

Insbesondere sollten Konstruktoren die Oberklassen-Konstruktoren bis zur aller-ersten Basisklasse aufrufen, so daß jeder Unterklassen-Konstruktor sich nur um dieErweiterungen kümmern muß, die in seiner Klasse zur direkten Oberklasse hinzu-gefügt wurden. Jeder Unterklassen-Destruktor sollte die Ressourcen der Unterklas-se freigeben und dann den Oberklassen-Destruktor aufrufen, ebenfalls ganz zurückbis zur ursprünglichen Basisklasse. Konstruktion erfolgt vom Vorfahren hin zurneuesten Unterklasse, Destruktion genau umgekehrt.

Unsere Strategie hat eine Schwachstelle: Im allgemeinen sollten wir von einemKonstruktor aus keine dynamisch gebundenen Methoden aufrufen, denn das Objektist vielleicht noch nicht ganz initialisiert. new() fügt die endgültige Typbeschreibungin ein Objekt ein und ruft dann erst den Konstruktor auf. Wenn also ein Konstruktoreine dynamisch gebundene Methode aufruft, erreicht er nicht unbedingt die Metho-de in seiner eigenen Klasse. Sicherheitshalber müßte der Konstruktor die Methodemit ihrem internen Namen in der eigenen Klasse aufrufen, also bei Punkten zumBeispiel Point_draw() und nicht etwa draw().

Um Information möglichst gut zu verbergen, implementieren wir eine Klassemit drei Dateien. Die Schnittstellendatei enthält die Deklaration des abstrakten Da-tentyps, die Repräsentierungsdatei enthält die Struktur eines Objekts, und die Quell-datei enthält den Code der Methoden und initialisiert die Typbeschreibung. EineSchnittstellendatei fügt die Schnittstellendatei der Oberklasse ein und wird in derQuelldatei und bei jeder Anwendung eingefügt. Eine Repräsentierungsdateibenötigt die Repräsentierungsdatei der Oberklasse und wird nur bei der Implemen-tierung einer Klasse benützt.

Page 55: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

45___________________________________________________________________________4.9 Ist sie oder hat sie? — Vererbung kontra Aggregate

Komponenten einer Oberklasse sollten in einer Unterklasse nicht direkt verwen-det werden. Statt dessen können wir entweder statisch gebundene Zugriffs- undmöglicherweise auch Zuweisungsmethoden für jede Komponente bereitstellen,oder wir definieren geeignete Makros in der Repräsentierungsdatei der Oberklasse.Ein Komponentenzugriff erfolgt dann in jedem Fall im Stil eines Funktionsaufrufs.Damit kann man wesentlich leichter mit einem Editor oder Debugger nach Fehlernim Informationsfluß oder Zerstörung von Invarianten suchen.

4.9 Ist sie oder hat sie? — Vererbung kontra AggregateUnsere Repräsentierung für einen Kreis enthält die Repräsentierung für einen Punktals erste Komponente von struct Circle:

struct Circle { const struct Point _; int rad; };

Wir haben jedoch freiwillig beschlossen, auf diese Komponente nicht direkt zuzu-greifen. Wenn wir erben wollen, wandeln wir statt dessen von Circle zurück zuPoint um und bearbeiten struct Point dort.

Wir können einen Kreis auch anders repräsentieren: Er kann einen Punkt alsAggregat enthalten. Wir greifen auf Objekte nur über Zeiger zu, also würde dieseRepräsentierung eines Kreises ungefähr so aussehen:

struct Circle2 { struct Point * point; int rad; };

Dieser Kreis sieht nicht mehr wie ein Punkt aus, das heißt, jetzt können wir nichtvon Point erben und Methoden wiederverwenden. Dieser Kreis kann zwar Point-Methoden auf seine Point-Komponente anwenden, er kann nur keine Point-Methoden auf sich selbst anwenden.

Wenn eine Sprache Vererbung explizit syntaktisch regelt, ist der Unterschiedleichter zu erkennen. Ähnliche Repräsentierungen könnten in C++ etwa so ausse-hen:

struct Circle : Point { int rad; }; // Vererbung

struct Circle2 {struct Point point; int rad; // Aggregat

};

In C++ müssen wir auf Objekte nicht unbedingt nur über Zeiger zugreifen.

Vererbung, das heißt, die Entwicklung einer Unterklasse aus einer Oberklasse,und Aggregate, das heißt, das Einfügen eines Objekts als Komponente eines ande-ren Objekts, bieten ziemlich ähnliche Möglichkeiten. Was man in einem bestimm-ten Entwurf verwendet, kann man oft mit der Frage ist-sie-oder-hat-sie? entschei-den: Wenn ein Objekt einer neuen Klasse so ist wie ein Objekt einer anderen Klas-se, sollten wir die neue Klasse mit Hilfe von Vererbung implementieren; wenn einObjekt einer neuen Klasse ein Objekt einer anderen Klasse als Teil seines Zustandshat, sollten wir ein Aggregat aufbauen.

Soweit es unsere Punkte betrifft, ist ein Kreis einfach ein großer Punkt, deshalbhaben wir Vererbung verwendet, um Kreise zu implementieren. Ein Rechteck istein weniger klares Beispiel: Wir können es mit einem Bezugspunkt und Seitenlän-

Page 56: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

46___________________________________________________________________________4 Vererbung — Code wiederverwenden und anpassen

gen beschreiben, aber auch durch die Endpunkte einer Diagonalen oder gar durchdrei Eckpunkte. Nur mit einem Bezugspunkt ist ein Rechteck eine Art von Punkt,die anderen Repräsentierungen laufen auf Aggregate hinaus. Bei unseren arithmeti-schen Ausdrücken hätten wir zwar Vererbung benützen können, um von einemunären zu einem binären Knoten zu kommen, aber das hätte dem Test ernsthaft wi-dersprochen.

4.10 Mehrfache VererbungDa wir nur ANSI-C verwenden, können wir nicht verschleiern, daß Vererbung bedeu-tet, eine Struktur am Anfang einer anderen einzufügen. Aufwärts umzuwandeln istder Schlüssel zur Wiederverwendung einer Oberklassen-Methode für Unterklas-sen-Objekte. Einen Kreis wandeln wir dadurch aufwärts in einen Punkt um, daß wirdie Adresse des Strukturanfangs umwandeln; dabei ändert sich dieser Adreßwertselbst nicht.

Wenn wir zwei oder noch mehr Strukturen in eine andere Struktur einfügen,und wenn wir beim Aufwärts-Umwandeln einige Adreßrechnungen in Kauf nehmen,könnten wir das Resultat Mehrfachvererbung nennen: Ein Objekt kann sich so ver-halten, als ob es zu mehreren anderen Klassen gehört. Der Vorteil scheint zu sein,daß wir die Vererbungsverhältnisse nicht sehr sorgfältig entwerfen müssen — wirkönnen Klassen schnell zusammenbringen und ziemlich beliebig erben. Der Nach-teil ist offensichtlich, daß bei den Aufwärts-Umwandlungen Adreßrechnungen nötigsind, bevor wir die Methoden der Oberklassen wiederverwenden können.

Tatsächlich kann es sehr schnell sehr unübersichtlich werden. Betrachten wirdazu einen Text und ein Rechteck, jeweils mit einem eingeerbten Bezugspunkt.Wir können sie zu einem Knopf verbinden — nur bleibt die Frage, ob der Knopf danneinen oder zwei Bezugspunkte erben soll. C++ erlaubt beide Möglichkeiten, mitreichlich obskuren Regeln für Konstruktion und up-cast .

Da wir alles direkt in ANSI-C erledigen, haben wir einen entscheidenden Vorteil:ANSI-C versteckt die Tatsache nicht, daß Vererbung — einfach oder mehrfach — im-mer durch Einfügen erfolgt. Einfügen kann man aber auch in einem Aggregat. Esist keineswegs klar, daß mehrfache Vererbung mehr für den Programmierer leistet,als die Sprachdefinition komplizierter und die Implementierung aufwendiger zu ma-chen. Wir lassen’s einfach und machen nur mit einfacher Vererbung weiter. Imvierzehnten Kapitel werden wir sehen, daß eine der wesentlichen Anwendungen fürmehrfache Vererbung, das Verknüpfen verschiedener Bibliotheken, oft auch durchAggregate und Weiterreichen von Nachrichten gelöst werden kann.

4.11 ÜberlegungenGrafikprogrammierung bietet viele Möglichkeiten für Vererbung: Ein Punkt und eineSeitenlänge definieren ein Quadrat; ein Punkt und zwei Abstände definieren einRechteck, ein Liniensegment oder eine Ellipse; ein Punkt und ein Vektor von relati-ven Koordinaten definieren ein Polygon oder sogar eine Spline-Kurve. Bevor wir allediese Klassen konstruieren, können wir raffiniertere Punkte einführen, indem wir

Page 57: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

47___________________________________________________________________________4.11 Überlegungen

Text mit relativer Positionierung oder Farbe und andere grafische Attribute ein-führen.

move() dynamisch zu binden ist schwierig, aber vielleicht interessant: Fixe Ob-jekte könnten beschließen, ihren Bezugspunkt festzuhalten und nur ihren Text zubewegen.

Vererbung kann man in vielen anderen Bereichen finden: Set, Bag und andereZusammenstellungen wie Listen, Stapel, Warteschlangen etc. sind verwandte Da-tentypen; Zeichenketten, Atome und Variablen mit Namen und Wert bilden eine an-dere Familie.

Oberklassen können Algorithmen verpacken. Wenn wir annehmen, daß dyna-misch gebundene Methoden zum Vergleichen und Vertauschen von Elementen ineiner Sammlung von Objekten abhängig von positiven Indexwerten existieren, kön-nen wir eine Oberklasse mit einem Sortieralgorithmus implementieren. Unterklas-sen müssen dann ihre Objekte in einem Vektor vergleichen und vertauschen kön-nen, aber sie erben, daß sie sortiert werden können.

Wir können jetzt auch anfangen, Programmierwerkzeuge zu implementieren,zum Beispiel ein einfaches Shell-Skript, das die Repräsentierung einer Klasse aus ei-ner Implementierung herauszieht und die opake Repräsentierungsdatei produziert,die wir am Ende von Abschnitt 4.6 gesehen haben.

Page 58: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 59: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

49___________________________________________________________________________

5Programmierpraxis

Symboltabelle

Mühsame Konstruktionen auf der Basis von union kann man oft elegant dadurch

vermeiden, daß man eine Struktur verlängert und dadurch für verschiedene Zwecke

spezialisiert, wobei man die Funktionalität der Basisstruktur übernimmt. Insbeson-

dere in Verbindung mit dynamischer Bindung erhalten wir eine einheitliche und ro-

buste Technik, um mit divergierender Information fertigzuwerden. Wenn der

grundsätzliche Mechanismus steht, können wir eine neue, erweiterte Struktur leicht

hinzunehmen und die Grundfunktionen wiederverwenden.

Als Beispiel fügen wir reservierte Worte, Konstanten, Variablen und mathemati-

sche Funktionen zu der kleinen Dialogsprache hinzu, die wir im dritten Kapitel imple-

mentiert haben. Alle diese Objekte leben in einer Symboltabelle und verwenden

den gleichen Suchmechanismus für Namen.

5.1 Namen erkennenIm Abschnitt 3.2 haben wir die Funktion scan() implementiert, die eine Eingabezeile

vom Hauptprogramm bekommt und ein Eingabesymbol pro Aufruf liefert. Wenn

wir reservierte Worte, benannte Konstanten etc. verwenden wollen, müssen wir

scan() erweitern. Genau wie Gleitkommazahlen extrahieren wir auch alphanumeri-

sche Zeichenketten zur weiteren Analyse:

#define ALNUM "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \"abcdefghijklmnopqrstuvwxyz" \"_" "0123456789"

static enum tokens scan (const char * buf){ static const char * bp;

...if (isdigit(* bp) || * bp == ’.’)

...else if (isalpha(* bp) || * bp == ’_’){ char buf [BUFSIZ];

int len = strspn(bp, ALNUM);

if (len >= BUFSIZ)error("name too long: %-.10s...", bp);

strncpy(buf, bp, len), buf[len] = ’\0’, bp += len;token = screen(buf);

}...

Wenn wir einen Namen gefunden haben, lassen wir eine neue Funktion screen()entscheiden, welcher Wert dafür in token abgelegt werden soll. Bei Bedarf hinter-

legt screen() eine Beschreibung des Symbols in einer globalen Variablen symbol,die der Rest des Programms inspizieren kann.

Page 60: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

50___________________________________________________________________________5 Programmierpraxis — Symboltabelle

5.2 VariablenEine Variable ist an zwei Operationen beteiligt: Ihr Wert wird als Operand in einem

Ausdruck benützt, oder der Wert eines Ausdrucks wird der Variablen zugewiesen.

Die erste Operation ist eine einfache Erweiterung in der Funktion factor(), die als

Teil des Parsers im Abschnitt 3.5 gezeigt wurde.

static void * factor (void){ void * result;

...switch (token) {case VAR:

result = symbol;break;

...

VAR ist ein eindeutiger Wert, den screen() in token ablegt, wenn ein geeigneter Na-

me gefunden wurde. Mehr Information über den Namen steht in der globalen Varia-

blen symbol. In diesem Fall enthält symbol einen Knoten, der die Variable als Blatt

im Baum für einen arithmetischen Ausdruck repräsentiert. Entweder screen() fin-

det die Variable in der Symboltabelle, oder wir benützen die Beschreibung Var, um

eine neue Variable zu erzeugen.

Eine Zuweisung zu erkennen, ist ein bißchen schwieriger. Unsere Dialogspra-

che ist bequem zu benützen, wenn wir zwei Arten von Anweisungen mit folgender

Syntax erlauben:

asgn : sum| VAR = asgn

Leider kann aber VAR auch am linken Ende von sum stehen, das heißt, es ist nicht

sofort klar, wie wir eine Zuweisung im C-Stil auf der Basis von recursive descent er-

kennen.* Da wir aber ohnehin reservierte Worte einführen wollen, begnügen wir

uns mit folgender Grammatik:

stmt : sum| LET VAR = sum

Dies ergibt folgende Funktion:

static void * stmt (void){ void * result;

switch (token) {case LET:

if (scan(0) != VAR)error("bad assignment");

result = symbol;if (scan(0) != ’=’)

error("expecting =");

____________________________________________________________________________________________

* Es gibt einen Trick: Wir rufen einfach sum() auf. Wenn anschließend das nächste Eingabesymbol =ist, muß sum() einen Baum konstruiert haben, der nur aus einem Blatt für eine Variable besteht, und wir

können die Zuweisung aufbauen.

Page 61: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

51___________________________________________________________________________5.3 Einträge in der Symboltabelle — ‘‘Name’’

scan(0);return new(Assign, result, sum());

default:return sum();

}}

Im Hauptprogramm rufen wir jetzt stmt() an Stelle von sum() auf, und unser Erken-

ner kann bereits mit Variablen umgehen. Assign ist eine neue Typbeschreibung für

einen Knoten, der den Wert einer Summe berechnet und einer Variablen zuweist.

5.3 Einträge in der Symboltabelle — NameEine Zuweisung hat folgende Syntax:

stmt : sum| LET VAR = sum

LET ist ein Beispiel für ein reserviertes Wort. Wenn wir screen() implementieren,

können wir immer noch entscheiden, was für eine alphanumerische Eingabe als LETerkannt werden soll: scan() extrahiert die Eingabe aus der Eingabezeile und über-

gibt sie an screen(). Diese Funktion durchsucht die Symboltabelle, liefert den zu-

gehörigen Wert für token und hinterlegt, wenigstens für eine Variable, einen Knoten

in symbol.Der Erkenner verwendet LET nicht weiter, aber er montiert eine Variable als

Blatt im Baum. Bei anderen Symbolen, wie zum Beispiel dem Namen einer mathe-

matischen Funktion, werden wir vielleicht new() auf eine Beschreibung anwenden,

die screen() als symbol hinterlegt, um einen neuen Knoten für unseren Baum zu

bekommen. Einträge in unserer Symboltabelle sollten deshalb in der Regel die glei-

chen Funktionen mit dynamischer Bindung besitzen wie die Knoten im Baum.

Für ein reserviertes Wort sollte ein Name-Objekt den zugehörigen Eingabetext

und den Wert für token enthalten. Da wir später von Name erben wollen, definie-

ren wir die Struktur in einer Repräsentierungsdatei Name.r:struct Name { /* Basisstruktur */

const void * type; /* fuer dynamische Bindung */const char * name; /* kann von malloc stammen */int token;

};

Unsere Symbole sterben nie: Es spielt keine Rolle, ob ihre Namen konstante Zei-

chenketten für reservierte Worte oder dynamisch gespeicherte Zeichenketten für

benutzerdefinierte Variablen sind — wir werden sie keinesfalls freigeben.

Bevor wir ein Symbol finden können, müssen wir es in die Symboltabelle eintra-

gen. Das dürfen wir allerdings nicht einfach in Form von new(Name, ...) implemen-

tieren, denn wir wollen kompliziertere Symbole als nur Name einführen, und wir

sollten die Implementierung der Symboltabelle vor ihnen verstecken. Statt dessen

implementieren wir eine Funktion install(), die ein Name-Objekt erhält und in die

Symboltabelle einfügt. Hier ist die Schnittstellendatei Name.h für die Symboltabel-

le:

Page 62: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

52___________________________________________________________________________5 Programmierpraxis — Symboltabelle

extern void * symbol; /* -> neuester Name von screen() */

void install (const void * symbol);int screen (const char * name);

Der Erkenner muß reservierte Worte wie LET in die Symboltabelle eintragen,

bevor sie screen() finden kann. Diese Worte können in einer konstanten Tabelle

mit Strukturen definiert werden — dies spielt keine Rolle für install(). Die folgende

Funktion trägt die reservierten Worte ein und initialisiert so den Erkenner:

#include "Name.h"#include "Name.r"

static void initNames (void){ static const struct Name names [] = {

{ 0, "let", LET },0 };

const struct Name * np;

for (np = names; np -> name; ++ np)install(np);

}

names[], die Tabelle der reservierten Worte, muß nicht sortiert sein. Um names[]zu definieren, verwenden wir die Repräsentierung von Name, das heißt, wir fügen

Name.r ein. Da das reservierte Wort LET nicht in einen Baum eingebaut wird,

benötigen wir keine dynamisch gebundenen Methoden, deshalb steht ein Nullzeiger

am Anfang jedes Elements von names[].

5.4 Implementierung der Oberklasse — NameSymbole per Namen zu suchen, ist ein Standardproblem. Leider definiert der ANSI-

Standard dafür keine brauchbare Bibliotheksfunktion. bsearch() — binäre Suche in

einer sortierten Tabelle — löst das Problem beinahe, aber wenn wir ein einziges

neues Symbol in die Tabelle einfügen, müssen wir qsort() aufrufen, bevor wir wie-

der mit bsearch() suchen können.

UNIX Systeme liefern wahrscheinlich zwei oder drei Funktionsfamilien, die sich

mit wachsenden Tabellen beschäftigen. lsearch() — lineare Suche in einem Vektor

und Hinzufügen am Ende(!) — ist nicht unbedingt effizient. hsearch() — eine

Hash-Tabelle für Strukturen, die aus Text und einem Informationszeiger bestehen —

unterhält nur eine einzige Tabelle fixer Größe und erzwingt eine unpraktische Struk-

tur für die Einträge. tsearch() — ein binärer Baum mit beliebigem Vergleich und Lö-

schen — ist zwar die allgemeinste Familie von Funktionen, zugleich aber sehr ineffi-

zient, wenn Symbole als sortierte Folge installiert werden.

Für ein UNIX System ist tsearch() wahrscheinlich der beste Kompromiß. Die

Quellen für eine portable Implementierung mit sogenannten binary threaded treesfinden sich in [Sch87]. Wenn diese Funktionsfamilie allerdings nicht zur Verfügung

steht, oder wenn wir mit sortiertem Einfügen rechnen müssen, sollten wir ein einfa-

cheres Verfahren implementieren. Es zeigt sich, daß eine sorgfältige Implementie-

rung von bsearch() sehr leicht erweitert werden kann, um das Einfügen in einen

sortierten Vektor zu realisieren:

Page 63: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

53___________________________________________________________________________5.4 Implementierung der Oberklasse — ‘‘Name’’

void * binary (const void * key,void * _base, size_t * nelp, size_t width,int (* cmp) (const void * key, const void * elt))

{ size_t nel = * nelp;#define base (* (char **) & _base)

char * lim = base + nel * width, * high;

if (nel > 0){ for (high = lim - width; base <= high; nel >>= 1)

{ char * mid = base + (nel >> 1) * width;int c = cmp(key, mid);

if (c < 0)high = mid - width;

else if (c > 0)base = mid + width, -- nel;

elsereturn (void *) mid;

}

Bis zu diesem Punkt ist das die normale binäre Suche in einem beliebigen, sortier-

ten Vektor. key zeigt auf das Objekt, das gefunden werden soll, base ist zuerst die

Anfangsadresse einer Tabelle von *nelp Elementen, von denen jedes width Bytes

hat; und cmp ist eine Funktion, die key mit einem Tabelleneintrag vergleicht. An

diesem Punkt haben wir entweder ein Tabellenelement gefunden und seine Adres-

se als Resultat geliefert, oder base ist jetzt die Adresse, bei der key in der Tabelle

stehen sollte. Wir fahren folgendermaßen fort:

memmove(base + width, base, lim - base);}++ *nelp;return memcpy(base, key, width);

#undef base}

memmove() verschiebt das Vektorende,* und memcpy() fügt key ein. Wir neh-

men an, daß hinter dem belegten Teil des Vektors noch Platz ist, und wir notieren

mit Hilfe von nelp, daß wir ein Element hinzugefügt haben — binary() unterschei-

det sich von der Standardfunktion bsearch() nur dadurch, daß die Adresse und nicht

der Wert der Variablen benötigt wird, die die Anzahl der Elemente in der Tabelle

enthält.

Nachdem wir jetzt ein allgemeines Verfahren für Suche und Eintrag besitzen,

können wir unsere Symboltabelle sehr leicht verwalten. Als erstes müssen wir

einen Schlüssel mit einem Tabelleneintrag vergleichen:

static int cmp (const void * _key, const void * _elt){ const char * const * key = _key;

const struct Name * const * elt = _elt;

return strcmp(* key, (* elt) -> name);}

____________________________________________________________________________________________

* memmove() kopiert Bytes auch dann, wenn Quelle und Ziel überlappen; memcpy() kann das nicht, ist

dafür aber möglicherweise effizienter.

Page 64: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

54___________________________________________________________________________5 Programmierpraxis — Symboltabelle

Als Schlüssel liefern wir nur die Adresse des Zeigers auf den Text eines Eingabe-

symbols. Die Tabelleneinträge sind natürlich Name-Strukturen, und wir betrachten

nur die Komponente .name.

Suchen oder Eintragen erledigen wir durch einen Aufruf von binary() mit geeig-

neten Parametern. Da wir die Anzahl der Symbole nicht kennen, sorgen wir dafür,

daß immer noch Platz in der Tabelle ist:

static struct Name ** search (const char ** name){ static const struct Name ** names; /* dynamisch */

static size_t used, max;

if (used >= max){ names = names

? realloc(names, (max *= 2) * sizeof * names): malloc((max = NAMES) * sizeof * names);

assert(names);}return binary(name, names, & used, sizeof * names, cmp);

}

NAMES ist eine definierte Konstante, die die anfängliche Größe der Tabelle festlegt;

geht der Platz aus, verdoppeln wir die Größe.

search() erhält die Adresse eines Zeigers auf den Text, der gefunden werden

soll, und liefert die Adresse des Tabelleneintrags. Wenn der Text in der Tabelle

nicht gefunden werden kann, trägt binary() den Suchschlüssel ein — das heißt, nur

den Zeiger auf den Text, keine struct Name. Diese Strategie soll screen() helfen,

denn diese Funktion muß nur dann einen neuen Tabelleneintrag anlegen, wenn ein

Name aus der Eingabe wirklich unbekannt ist:

int screen (const char * name){ struct Name ** pp = search(& name);

if (* pp == (void *) name) /* name wurde eingetragen*/* pp = new(Var, name);

symbol = * pp;return (* pp) -> token;

}

screen() läßt search() nach dem Eingabesymbol suchen. Wenn der Zeiger auf den

Eingabetext in die Symboltabelle eingetragen wird, müssen wir ihn durch einen Ein-

trag ersetzen, der den neuen Namen repräsentiert.

Für screen() muß ein neuer Name eine Variable sein. Wir nehmen an, daß es

eine Typbeschreibung Var gibt, die weiß, wie man Name-Strukturen erzeugt, die

Variablen repräsentieren; alles übrige erledigt new(). In jedem Fall lassen wir die glo-

bale Variable symbol auf den Symboltabelleneintrag zeigen, und wir liefern seine

Komponente .token als Resultat.

void install (const void * np){ const char * name = ((struct Name *) np) -> name;

struct Name ** pp = search(& name);

Page 65: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

55___________________________________________________________________________5.5 Implementierung der Unterklasse — ‘‘Var’’

if (* pp != (void *) name)error("cannot install name twice: %s", name);

* pp = (struct Name *) np;}

install() ist etwas einfacher. Wir erhalten ein Name-Objekt und lassen search() es

in der Symboltabelle finden. install() wird sich nur mit neuen Symbolen beschäfti-

gen, also sollten wir immer ein Objekt an Stelle seines Namens eintragen können.

Wenn search() ein Symbol tatsächlich bereits in der Tabelle findet, gibt es Ärger.

5.5 Implementierung der Unterklasse — Varscreen() ruft new() auf, um ein neues Symbol für eine Variable zu erzeugen, und lie-

fert es an den Erkenner, der das Symbol in den Baum für einen Ausdruck einbaut.

Deshalb muß Var Symboltabelleneinträge erzeugen, die sich wie Knoten benehmen

können, das heißt, wenn wir struct Var vereinbaren, müssen wir struct Name er-

weitern, damit das Objekt in der Symboltabelle existieren kann, und wir müssen die

dynamisch gebundenen Funktionen vorsehen, die für Knoten in Ausdrücken ver-

wendet werden. Wir beschreiben die Schnittstelle in Var.h:

const void * Var;const void * Assign;

Eine Variable hat einen Namen und einen Wert. Wenn wir einen arithmetischen

Ausdruck bewerten, müssen wir die Komponente .value als Wert liefern. Wenn

wir einen Ausdruck freigeben, dürfen wir den Knoten einer Variablen nicht freige-

ben, denn er lebt ja auch in der Symboltabelle:

struct Var { struct Name _; double value; };

#define value(tree) (((struct Var *) tree) -> value)

static double doVar (const void * tree){

return value(tree);}

static void freeVar (void * tree){}

Wie in Abschnitt 4.6 besprochen, wird der Code dadurch vereinfacht, daß wir eine

Zugriffsfunktion für den Wert einführen.

Wir erzeugen eine Variable, indem wir eine Speicherfläche für struct Var anle-

gen und eine dynamische Kopie des Variablennamens sowie den Wert VAR als

.token für den Erkenner eintragen:

static void * mkVar (va_list ap){ struct Var * node = calloc(1, sizeof(struct Var));

const char * name = va_arg(ap, const char *);size_t len = strlen(name);

assert(node);node -> _.name = malloc(len+1);

Page 66: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

56___________________________________________________________________________5 Programmierpraxis — Symboltabelle

assert(node -> _.name);strcpy((void *) node -> _.name, name);node -> _.token = VAR;return node;

}

static struct Type _Var = { mkVar, doVar, freeVar };

const void * Var = & _Var;

new() fügt die Typbeschreibung Var in den Knoten ein, bevor das neue Symbol an

screen() oder einen anderen Abnehmer geliefert wird.

Technisch betrachtet ist mkVar() der Konstruktor für Name. Wir müssen aller-

dings nur Variablennamen dynamisch speichern. Da wir beschlossen haben, daß in

dieser Anwendung der Konstruktor selbst die Speicherfläche für sein Objekt anlegt,

kann der Var-Konstruktor keinen Name-Konstruktor aufrufen, damit dieser die Kom-

ponenten .name und .token kontrolliert — ein Name-Konstruktor würde structName statt struct Var anlegen.

5.6 ZuweisungZuweisung ist eine binäre Operation. Der Erkenner garantiert bei der Konstruktion

des Baums, daß wir eine Variable als rechten und eine Summe als linken Operan-

den haben. Deshalb müssen wir wirklich nur die eigentliche Zuweisung implemen-

tieren, das heißt, die Funktion, die dynamisch als Komponente .exec der Typbe-

schreibung gebunden wird:

#include "value.h"#include "value.r"

static double doAssign (const void * tree){

return value(left(tree)) = exec(right(tree));}

static struct Type _Assign = { mkBin, doAssign, freeBin };

const void * Assign = & _Assign;

Wir benützen den Konstruktor und Destruktor von Bin, die deshalb in der Imple-

mentierung der arithmetischen Operationen global sichtbar definiert werden müs-

sen. Wir holen uns auch struct Bin und die Zugriffsfunktionen left() und right().Dies wird alles in der Schnittstellendatei value.h und in der Repräsentierungsdatei

value.r exportiert. Unsere eigene Zugriffsfunktion value() für struct Var erlaubt ab-

sichtlich eine Modifikation, damit die Zuweisung ganz elegant implementiert werden

kann.

5.7 Noch eine Unterklasse — KonstantenWer tippt schon gern den Wert von π oder anderen mathematischen Konstanten?

Wir übernehmen eine Idee aus Kernighan und Pikes hoc [K&P86] und definieren ei-

nige Konstanten für unsere Dialogsprache. Die folgende Funktion muß bei der In-

itialisierung des Erkenners aufgerufen werden:

Page 67: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

57___________________________________________________________________________5.8 Mathematische Funktionen — ‘‘Math’’

void initConst (void){ static const struct Var constants [] = { /* wie hoc */

{ &_Var, "PI", CONST, 3.14159265358979323846 },...0 };

const struct Var * vp;

for (vp = constants; vp -> _.name; ++ vp)install(vp);

}

Variablen und Konstanten sind fast dasselbe: Beide besitzen Namen und Wert

und leben in der Symboltabelle, beide liefern ihren Wert in einem arithmetischen

Ausdruck, und beide sollten nicht gelöscht werden, wenn wir einen arithmetischen

Ausdruck löschen. Wir dürfen allerdings einer Konstanten nichts zuweisen, folglich

müssen wir einen neuen Wert CONST vereinbaren, den der Erkenner in factor() ge-

nau wie VAR akzeptiert, der aber links als Ziel einer Zuweisung in stmt() nicht er-

laubt ist.

5.8 Mathematische Funktionen — MathANSI-C definiert eine Reihe von mathematischen Funktionen wie sin(), sqrt(), exp()etc. Als eine weitere Übung zur Vererbung soll unsere Dialogsprache Bibliotheks-

funktionen mit einem einzigen double-Wert als Parameter und einem double-

Resultat verwenden.

Diese Funktionen arbeiten so ähnlich wie unäre Operatoren. Wir könnten einen

neuen Knotentyp für jede Funktion erfinden und den größten Teil der Funktionalität

von Minus und der Name-Klasse übernehmen, aber es geht noch einfacher. Wir

verlängern struct Name in struct Math wie folgt:

struct Math { struct Name _;double (* funct) (double);

};

#define funct(tree) (((struct Math *) left(tree)) -> funct)

Zusätzlich zum Funktionsnamen, der in der Eingabe auftauchen muß, und dem Wert

für .token speichern wir die Adresse einer Bibliotheksfunktion wie sin() im Symbol-

tabelleneintrag.

Als Teil der Initialisierung rufen wir folgende Funktion auf, um alle Funktionsbe-

schreibungen in die Symboltabelle einzutragen:

#include <math.h>

void initMath (void){ static const struct Math functions [] = {

{ &_Math, "sqrt", MATH, sqrt },...0 };

const struct Math * mp;

Page 68: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

58___________________________________________________________________________5 Programmierpraxis — Symboltabelle

for (mp = functions; mp -> _.name; ++ mp)install(mp);

}

Ein Funktionsaufruf ist ein Faktor, genau wie ein Minuszeichen. Zur Erkennung

müssen wir unsere Grammatik für Faktoren erweitern:

factor : NUMBER| - factor| ...| MATH ( sum )

MATH ist der gemeinsame Wert in token für alle Funktionen, die initMath() einge-

tragen hat. Damit muß factor() im Erkenner folgendermaßen erweitert werden:

static void * factor (void){ void * result;

...switch (token) {case MATH:{ const struct Name * fp = symbol;

if (scan(0) != ’(’)error("expecting (");

scan(0);result = new(Math, fp, sum());if (token != ’)’)

error("expecting )");break;

}

symbol enthält zunächst den Symboltabelleneintrag für eine Funktion wie sin(). Wir

bewahren den Zeiger auf und konstruieren mit sum() den Baum für den Ausdruck,

der als Funktionsargument eingegeben wird. Dann verwenden wir Math, die Typ-

beschreibung für die Funktion, und lassen new() folgenden Knoten für den Aus-

drucksbaum erzeugen:

• •

struct Bin

sum•

"sin"

MATH

sin()

struct Math

mkBin()

doMath()

freeMath()

Math

Page 69: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

59___________________________________________________________________________5.9 Zusammenfassung

Die linke Seite eines binären Knotens zeigt auf den Symboltabelleneintrag für die

Funktion, und der Baum für das Argument ist rechts eingetragen. Der binäre Kno-

ten hat Math als Typbeschreibung, das heißt, die Methoden doMath() und

freeMath() werden aufgerufen, um den Knoten auszuführen beziehungsweise zu

löschen.

Der Math-Knoten wird noch immer mit mkBin() konstruiert, denn diese Funkti-

on ist unabhängig davon, was für Zeiger sie in den neuen Knoten einträgt.

freeMath() darf jedoch nur den rechten Unterbaum freigeben:

static void freeMath (void * tree){

delete(right(tree));free(tree);

}

Wenn wir das Bild genau ansehen, erkennen wir, daß die Ausführung eines

Math-Knotens sehr einfach ist. doMath() muß die Funktion aufrufen, die in dem

Symboltabellenelement gespeichert ist, das als linker Unterbaum des binären Kno-

tens eingetragen ist, von dem aus doMath() aufgerufen wird — dazu wurde ein-

gangs der Makro funct() definiert:

#include <errno.h>

static double doMath (const void * tree){ double result = exec(right(tree));

errno = 0;result = funct(tree)(result);if (errno)

error("error in %s: %s",((struct Math *) left(tree)) -> _.name,strerror(errno));

return result;}

Als einziges Problem müssen wir numerische Fehler entdecken, indem wir die Va-

riable errno beobachten, die in der ANSI-C Definitionsdatei errno.h deklariert ist. Da-

mit versteht unsere Dialogsprache auch mathematische Funktionen.

5.9 ZusammenfassungMit Hilfe einer Funktion binary(), die in einem sortierten Vektor suchen und einfü-

gen kann, haben wir eine Symboltabelle implementiert, die Strukturen mit einem

Namen und einem Symbolwert verwaltet. Dank Vererbung konnten wir andere

Strukturen in die Tabelle eintragen, ohne die Funktionen zum Suchen und Einfügen

zu ändern. Die Eleganz dieser Lösung wird erst richtig klar, wenn wir eine konven-

tionellere Vereinbarung eines Symboltabellenelements für unsere Aufgabe betrach-

ten:

Page 70: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

60___________________________________________________________________________5 Programmierpraxis — Symboltabelle

struct {const char * name;int token;union { /* je nach token */

double value;double (* funct) (double);

} u;};

Für reservierte Worte brauchen wir die union nicht, benutzerdefinierte Funktionen

bedingen eine wesentlich aufwendigere Beschreibung, und Zugriff auf Teile der

union ist unnötig kompliziert.

Dank Vererbung können wir die Funktionalität der Symboltabelle für neue Einträ-

ge anwenden, ohne den existenten Code zu verändern. Dynamische Bindung hilft

an vielen Stellen, die Implementierung einfach zu halten: Symboltabelleneinträge

für Konstanten, Variablen und Funktionen können in den Baum für einen Ausdruck

eingebunden werden, ohne daß wir Gefahr laufen, sie versehentlich zu löschen; ei-

ne bei .exec eingetragene Funktion muß sich immer nur mit ihrem eigenen, lokalen

Arrangement von Knoten beschäftigen.

5.10 ÜberlegungenWir benötigen neue reservierte Worte, um Dinge wie while- oder repeat-Schleifen,

if-Anweisungen etc. zu implementieren. Die Erkennung erfolgt in stmt(), aber dies

ist praktisch nur ein Problem der Compiler-Konstruktion, nicht von Vererbung.

Wenn wir die Art der Anweisung erkannt haben, konstruieren wir Knoten wie

While, Repeat oder IfElse, und die reservierten Worte in der Symboltabelle wissen

vermutlich nichts von ihrer Existenz.

Etwas interessanter sind Funktionen mit zwei Argumenten wie atan2() in der

mathematischen Bibliothek von ANSI-C. Aus der Sicht der Symboltabelle werden

diese Funktionen wie einfache Funktionen behandelt, aber für unseren Ausdrucks-

baum müssen wir einen neuen Knotentyp mit drei Abkömmlingen einführen, den

wir allerdings auch für IfElse verwenden können.

Benutzerdefinierte Funktionen sind ein lohnendes Problem. Das ist nicht allzu

schwer, wenn wir einen einzigen Parameter mit $ repräsentieren und einen Knoten-

typ Parm auf den Funktionseintrag in der Symboltabelle zeigen lassen, wo wir den

Argumentwert temporär speichern können, wenn wir Rekursion verbieten. Rekursi-

ve Funktionen mit Parameternamen und mehreren Parametern sind natürlich

schwieriger. Sie sind jedoch eine gute Übung, um die Vorteile von Vererbung und

dynamischer Bindung zu studieren. Wir werden im elften Kapitel zu diesem Pro-

blem zurückkehren.

Page 71: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

61___________________________________________________________________________

6Klassenhierarchie

Pflege vereinfachen

6.1 ForderungenDurch Vererbung können wir aus allgemeinen Datentypen spezialisiertere ent-wickeln, wobei wir die grundsätzlichen Funktionen nicht neu codieren müssen.Durch dynamische Bindung können wir Einschränkungen reparieren, die ein allge-meinerer Datentyp vielleicht hat. Was wir jetzt noch brauchen, ist eine saubere glo-bale Architektur, um ein größeres Klassensystem leicht zu pflegen:

(1) Alle dynamischen Bindungen müssen auf die richtigen Methoden zeigen —zum Beispiel darf ein Konstruktor nicht an der falschen Stelle in der Klassenbe-schreibung stehen.

(2) Wir brauchen ein kohärentes Verfahren, um dynamisch gebundene Methodenin einer Oberklasse hinzuzufügen, zu entfernen oder ihre Reihenfolge in der Be-schreibung zu ändern, das gleichzeitig korrekte Vererbung in die Unterklassensicherstellt.

(3) Es darf keine Schwachstellen wie fehlende dynamische Bindungen oder undefi-nierte Methoden geben.

(4) Wenn wir eine dynamisch gebundene Methode erben, muß die Implementie-rung der Oberklasse, von der wir erben, absolut unverändert bleiben, das heißt,Vererbung muß allein mit binärer Information möglich sein.

(5) Verschiedene Gruppen von Klassen müssen verschiedene dynamisch gebunde-ne Methoden haben können — zum Beispiel können draw() nur Point und Cir-cle aus dem vierten Kapitel, aber nicht die Mengen aus dem ersten Kapitel oderdie Knoten aus dem dritten und fünften Kapitel brauchen.

Diese Liste zeigt vor allem, daß die Pflege von dynamischen Bindungen schwierigund fehleranfällig ist — wenn wir die Situation nicht entscheidend verbessern kön-nen, haben wir sicher ein Problem.

Bisher haben wir mit einer einzigen Liste von dynamisch gebundenen Metho-den gearbeitet, unabhängig davon, ob sie für jede einzelne Klasse sinnvoll war. DieListe wurde als struct Class vereinbart, und sie wurde überall eingefügt, wo dyna-mische Bindung initialisiert werden mußte. Dank Funktionsprototypen wird ANSI-Cüberprüfen, ob Funktionsnamen wie Point_ctor in die Positionen in der Klassenbe-schreibung passen, für die sie als Initialisierung verwendet werden. Punkt (1) istnur ein Problem, wenn verschiedene Methoden typkompatible Schnittstellen besit-zen, oder wenn wir struct Class ändern und schlampig neu übersetzen.

Punkt (2), eine Änderung in struct Class, klingt wie ein Alptraum — wir müssenjede Klassenimplementierung von Hand suchen und die statische Initialisierung derKlassenbeschreibung korrigieren, und wir können sehr leicht vergessen, eine neueMethode in irgendeiner Klasse hinzuzufügen, womit dann Problem (3) entsteht.

Page 72: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

62___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

Im Abschnitt 5.6 hatten wir ein sehr elegantes Verfahren, um die Zuweisung inder Dialogsprache zu realisieren: Wir änderten die Quellen und machten dadurchdie dynamisch gebundenen Methoden für binäre Knoten aus Abschnitt 3.6 öffent-lich, damit wir sie zur Initialisierung von Assign verwenden konnten, aber dies wi-derspricht klar der Forderung (4).

Während schon die Pflege einer einzigen struct Class problematisch erscheint,ergibt sich aus Punkt (5), daß wir sogar verschiedene Versionen von struct Classfür verschiedene Gruppen von Klassen haben sollten! Die Forderung ist jedoch lei-der völlig plausibel: Jede Klasse braucht einen Konstruktor und einen Destruktor;für Punkte, Kreise und andere grafische Objekte benötigen wir grafische Methoden;Atome und Zeichenketten benutzen Vergleiche; Set, Bag, Listen oder andereSammlungen sollten Methoden zum Einfügen, Suchen und Entfernen von Objektenhaben etc.

6.2 MetaklassenEs stellt sich heraus, daß Forderung (5) unsere Probleme nicht vergrößert, sondernsogar aufzeigt, wie wir sie lösen können. Genauso wie ein Kreis Information zu ei-nem Punkt hinzufügt, fügen die Klassenbeschreibungen von Punkten und Kreisengemeinsam Information — nämlich eine polymorphe Methode draw() — zur Klas-senbeschreibung dieser beiden Klassen hinzu.

Anders formuliert: Solange zwei Klassen die gleichen dynamisch gebundenenMethoden besitzen, auch wenn diese vielleicht verschieden implementiert sind,können wir trotzdem die gleiche struct Class verwenden, um die dynamischen Bin-dungen zu speichern — dies ist der Fall für Point und Circle. Wenn wir allerdings ei-ne neue dynamisch gebundene Methode einführen, müssen wir struct Class ver-längern, um Platz für den neuen Methodenzeiger zu schaffen — so kommen wirvon einer Klasse, die nur einen Konstruktor und Destruktor besitzt, zu einer Klassen-beschreibung wie Point mit der zusätzlichen Komponente .draw.

Das Verlängern von Strukturen haben wir als Vererbung bezeichnet, das heißt,wir entdecken hier, daß Klassenbeschreibungen mit der gleichen Menge von Me-thoden selbst eine Klasse bilden, und daß es Vererbung zwischen Klassen von Klas-senbeschreibungen gibt!

Wir nennen eine Klasse von Klassenbeschreibungen eine Metaklasse. Eine Me-taklasse benimmt sich genau wie eine Klasse: Point und Circle, die Beschreibun-gen aller Punkte und aller Kreise, sind selbst zwei Objekte in einer MetaklassePointClass, denn sie können beide beschreiben, wie man zeichnet. Eine Metaklas-se hat auch Methoden: Wir können ein Objekt wie Point oder Circle nach derGröße der Objekte — Punkte oder Kreise — fragen, die es beschreibt, oder wir kön-nen das Objekt Circle fragen, ob Point tatsächlich die Oberklasse für Kreise be-schreibt.

Dynamisch gebundene Methoden können verschiedene Dinge für Objekte ausverschiedenen Klassen tun. Benötigt eine Metaklasse dynamisch gebundene Me-thoden? Der Destruktor in PointClass würde aufgerufen werden, wenndelete(Point) oder delete(Circle) ausgeführt wird, das heißt, wenn wir die Klassen-beschreibung für Punkte oder Kreise eliminieren. Dieser Destruktor sollte einen

Page 73: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

63___________________________________________________________________________6.3 Die Wurzeln — ‘‘Object’’ und ‘‘Class’’

Nullzeiger liefern, denn es ist sicher keine gute Idee, eine Klassenbeschreibung zueliminieren. Ein Metaklassen-Konstruktor ist wesentlich nützlicher:

Circle = new(PointClass, /* die Metaklasse */"Circle", /* erzeugt eine Beschreibung */Point, /* mit dieser Oberklasse, */sizeof(struct Circle), /* dieser Objektgroesse, */ctor, Circle_ctor, /* diesem Konstruktor */draw, Circle_draw, /* und dieser Zeichenmethode. */0); /* Ende der Liste */

Dieser Aufruf sollte eine Klassenbeschreibung für eine Klasse produzieren, derenObjekte konstruiert, zerstört und gezeichnet werden können. Da draw die neueIdee ist, die alle Klassenbeschreibungen in PointClass gemeinsam haben, könnenwir vernünftigerweise erwarten, daß der PointClass-Konstruktor wenigstens weiß,wie man die dynamische Bindung einer Zeichenmethode in der neuen Beschrei-bung hinterlegt.

Noch mehr kann erreicht werden: Wenn wir die Oberklassen-BeschreibungPoint an den PointClass-Konstruktor übergeben, sollte er in der Lage sein, zuerstalle vererbten dynamischen Bindungen von Point zu Circle zu kopieren und danngenau die zu überschreiben, die in Circle neu definiert werden. Dies löst jedochschon unser Problem (4) der binären Vererbung: Wenn wir Circle so erzeugen,geben wir nur noch die neuen Methoden an, die für Kreise spezifisch sind; dieMethoden für Punkte werden implizit geerbt, denn ihre Adressen können vomPointClass-Konstruktor kopiert werden.

6.3 Die Wurzeln — Object und ClassKlassenbeschreibungen mit der gleichen Menge von Methoden sind die Objekte ei-ner Metaklasse. Eine Metaklasse ist selbst auch eine Klasse und hat daher eineKlassenbeschreibung. Wir müssen annehmen, daß die Klassenbeschreibungen vonMetaklassen wiederum die Objekte von Metaklassen (Meta-Metaklassen?) sind, dienatürlich auch Klassen sind und...

Es ist vermutlich nicht sinnvoll, diesen Gedanken weiter zu verfolgen. Stattdessen beginnen wir lieber mit den primitivsten Objekten, die wir uns vorstellenkönnen. Wir definieren eine Klasse Object, deren Objekte wir erzeugen, zerstören,vergleichen und anzeigen können.

Schnittstelle in Object.h:

extern const void * Object; /* new(Object); */

void * new (const void * class, ...);void delete (void * self);

int differ (const void * self, const void * b);int puto (const void * self, FILE * fp);

Repräsentierung in Object.r:struct Object {

const struct Class * class; /* Typbeschreibung */};

Page 74: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

64___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

Als nächstes definieren wir die Repräsentierung der Klassenbeschreibung fürObject-Objekte, das heißt, die Struktur, auf die die Komponente .class in structObject für unsere trivialen Objekte zeigt. Beide Strukturen werden an den gleichenStellen gebraucht, also fügen wir zu Object.h folgendes hinzu:

extern const void * Class; /* new(Class, "name", super, sizesel, meth, ... 0); */

und zu Object.r:struct Class {

const struct Object _; /* Typbeschreibung */const char * name; /* Klassenname */const struct Class * super; /* Oberklasse */size_t size; /* Objektgroesse */void * (* ctor) (void * self, va_list * app);void * (* dtor) (void * self);int (* differ) (const void * self, const void * b);int (* puto) (const void * self, FILE * fp);

};

struct Class ist die Repräsentierung für jedes Element in der ersten MetaklasseClass. Diese Metaklasse ist eine Klasse, also müssen ihre Elemente auf eine Klas-senbeschreibung zeigen. Auf eine Klassenbeschreibung zeigen ist genau das, wasein Object tun kann, das heißt, struct Class erweitert struct Object, das heißt,Class ist eine Unterklasse von Object!

Das gibt keine Probleme: Objekte, das heißt, Instanzen der Klasse Object, kön-nen erzeugt, zerstört, verglichen und angezeigt werden. Wir haben beschlossen,daß wir Klassenbeschreibungen erzeugen wollen, und wir können einen Destruktorschreiben, der mehr oder weniger stillschweigend verhindert, daß eine Klassenbe-schreibung zerstört wird. Es ist vielleicht ganz nützlich, wenn wir Klassenbeschrei-bungen vergleichen und anzeigen können. Das bedeutet aber, daß die MetaklasseClass die gleiche Menge von Methoden und damit die gleiche Art von Klassenbe-schreibung hat wie die Klasse Object, das heißt, die Kette von Objekten zu ihrerKlassenbeschreibung und von dort zur Beschreibung der Klassenbeschreibung hörtdirekt hier auf. Korrekt initialisiert erhalten wir folgendes Bild:

Class

"Class"

Object

sizeof Object

Klasse erzeugen

return 0

vergleichen

anzeigen

struct Class

name

super

size

ctor

dtor

differ

puto

Object

"Object"

?

sizeof anObject

Objekt erzeugen

return self

vergleichen

anzeigen

struct Class

anObject

struct Object

Page 75: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

65___________________________________________________________________________6.4 Eine Unterklasse — ‘‘Any’’

Das Fragezeichen markiert eine ziemlich willkürliche Entscheidung: Hat Object eineOberklasse? Es ist eigentlich egal, aber um alles gleich zu behandeln, legen wirfest, daß Object seine eigene Oberklasse ist, das heißt, das Fragezeichen im Bildwird durch einen Zeiger auf Object selbst ersetzt.

6.4 Eine Unterklasse — AnyMit den Beschreibungen Class und Object können wir bereits neue Objekte undsogar eine neue Unterklasse erzeugen. Als Beispiel betrachten wir eine Unterklas-se Any, die behauptet, daß alle ihre Objekte gleich zu jedem beliebigen anderen Ob-jekt sind, das heißt, Any ersetzt differ() mit einer Funktion, die grundsätzlich Nullliefert. Hier ist die Implementierung von Any und ein einfaches Testprogramm, al-les in einer Datei any.c :

#include "Object.h"

static int Any_differ (const void * _self, const void * b){

return 0; /* Any ist nie verschieden... */}

int main (){ void * o = new(Object);

const void * Any =new(Class, "Any", Object, sizeOf(o),

differ, Any_differ,0);

void * a = new(Any);

puto(Any, stdout);puto(o, stdout);puto(a, stdout);

if (differ(o, o) == differ(a, a))puts("ok");

if (differ(o, a) != differ(a, o))puts("not commutative");

delete(o), delete(a);delete(Any);

return 0;}

Wenn wir eine neue Klasse implementieren, müssen wir die Schnittstelle ihrerOberklasse einfügen. Any hat die gleiche Repräsentierung wie Object, und dieKlasse ist so einfach, daß wir die Repräsentierungsdatei der Oberklasse nicht ein-mal benötigen. Die Klassenbeschreibung Any wird erzeugt, indem wir eine neueInstanz von ihrer Metaklasse Class verlangen und sie mit dem neuen Klassenna-men, der Oberklassen-Beschreibung und der Größe eines Objekts der neuen Klasseinitialisieren:

const void * Any =new(Class, "Any", Object, sizeOf(o),

differ, Any_differ,0);

Page 76: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

66___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

Außerdem geben wir genau die dynamisch gebundene Methode an, die wir in derneuen Klasse ersetzen. Methodennamen können in beliebiger Reihenfolge angege-ben werden, vor jedem steht sein Selektor-Name, und Null beendet die Liste.

Das Programm erzeugt eine Instanz o von Object und eine Instanz a von Anyund zeigt die neue Klassenbeschreibung und die zwei Instanzen. Jede Instanz kannnicht von sich selbst verschieden sein, also gibt das Programm ok aus. Die Metho-de differ() wurde für Any ersetzt, deshalb erhalten wir verschiedene Resultate,wenn wir o mit a oder eben umgekehrt vergleichen:

$ anyClass at 0x101fcObject at 0x101f4Any at 0x10220oknot commutativeAny: cannot destroy class

Natürlich sollten wir eine Klassenbeschreibung nicht zerstören können. Dieser Feh-ler wird schon beim Übersetzen entdeckt, denn delete() kann keinen Zeiger erhal-ten, der auf eine durch const geschützte Fläche zeigt.

6.5 Implementierung — ObjectDie Implementierung der Klasse Object ist völlig problemlos: Konstruktor und De-struktor liefern self, und differ() untersucht, ob seine beiden Argumentzeiger gleichsind. Es ist jedoch sehr wichtig, daß wir diese trivialen Implementierungen definie-ren: Wir arbeiten mit einem einzigen Baum von Klassen und machen Object zurendgültigen Oberklasse aller anderen Klassen; wenn eine Klasse eine Methode wiediffer() nicht ersetzt, erbt sie letztlich von Object, das heißt, jede Klasse hat minde-stens eine rudimentäre Definition für jede dynamisch gebundene Methode, dieschon auf Object angewendet werden kann.

Dahinter verbirgt sich ein allgemeines Sicherheitsprinzip: Immer wenn wir eineneue dynamisch gebundene Methode einführen, implementieren wir sie sofort fürihre erste Klasse. Damit können wir nie eine vollständig undefinierte Methode zuselektieren versuchen. Ein Beispiel hierzu ist die Methode puto() für Object:

static int Object_puto (const void * _self, FILE * fp){ const struct Class * class = classOf(_self);

return fprintf(fp, "%s at %p\n", class -> name, _self);}

Jedes Objekt zeigt auf eine Klassenbeschreibung, und dort haben wir den Klassen-namen gespeichert. Deshalb können wir für jedes Objekt wenigstens seinen Klas-sennamen und seine Adresse anzeigen. Die ersten drei Ausgabezeilen des trivialenTestprogramms in Abschnitt 6.4 zeigen, daß wir diese Methode für Class oder Anynicht ersetzt haben.

puto() beruht auf einer Zugriffsfunktion classOf(), die einige Dinge prüft unddann die Klassenbeschreibung eines Objekts liefert:

Page 77: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

67___________________________________________________________________________6.6 Implementierung — ‘‘Class’’

const void * classOf (const void * _self){ const struct Object * self = _self;

assert(self && self -> class);return self -> class;

}

Analog können wir ein Objekt nach seiner Größe fragen* — dies ist interessant,wenn wir uns daran erinnern, daß wir Objekte schlicht als void * in ANSI-C darstel-len:

size_t sizeOf (const void * _self){ const struct Class * class = classOf(_self);

return class -> size;}

Man kann diskutieren, ob wir ein Objekt nach seiner Größe fragen sollen, oder obwir ein Objekt nur nach seiner Klasse und dann die Klasse explizit nach der Objekt-größe fragen. Wenn wir sizeOf() für Objekte implementieren, können wir die Me-thode nicht auf eine Klassenbeschreibung anwenden, um ihre Objektgröße zu erhal-ten — sizeOf() liefert dann die Größe der Klassenbeschreibung selbst. Erst die Pra-xis zeigt, daß die Definition von sizeOf() für Objekte zweckmäßiger ist. Im Gegen-satz dazu ist super() eine statisch gebundene Methode, die die Oberklasse einerKlasse und nicht eines Objekts liefert.

6.6 Implementierung — ClassClass ist eine Unterklasse von Object, deshalb können wir die Methoden für Ver-gleich und Anzeige einfach erben. Der Destruktor liefert einen Nullzeiger und hältdamit delete() davon ab, die Fläche einer Klassenbeschreibung freizugeben:

static void * Class_dtor (void * _self){ struct Class * self = _self;

fprintf(stderr, "%s: cannot destroy class\n", self->name);return 0;

}

Hier ist die Zugriffsfunktion, um die Oberklasse einer Klassenbeschreibung zu be-kommen:

const void * super (const void * _self){ const struct Class * self = _self;

assert(self && self -> super);return self -> super;

}

Nur der Class-Konstruktor ist schwieriger zu implementieren, denn hier wird ei-ne neue Klassenbeschreibung initialisiert, hier wird vererbt, und hier kann man dieersten vier dynamisch gebundenen Methoden ersetzen. Wir erinnern uns, wie imAbschnitt 6.4 eine neue Klassenbeschreibung erzeugt wurde:____________________________________________________________________________________________

* Der Methodenname sizeOf() ist sicher fehleranfällig, aber ich konnte einfach dem Wortspiel nicht wi-derstehen. Gute Namen zu erfinden ist eine Kunst — und es lohnt sich.

Page 78: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

68___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

const void * Any =new(Class, "Any", Object, sizeOf(o),

differ, Any_differ,0);

Dies bedeutet, daß unser Class-Konstruktor den Namen, die Oberklasse und dieObjektgröße für eine neue Klassenbeschreibung erhält. Wir übertragen diese zuerstaus der Argumentliste:

static void * Class_ctor (void * _self, va_list * app){ struct Class * self = _self;

self -> name = va_arg(* app, char *);self -> super = va_arg(* app, struct Class *);self -> size = va_arg(* app, size_t);

assert(self -> super);

self kann kein Nullzeiger sein, denn sonst hätten wir diese Methode gar nicht er-reicht. super könnte jedoch Null sein, und das wäre wirklich falsch.

Der nächste Schritt ist Vererbung. Wir müssen den Konstruktor und alle ande-ren Methoden von der Oberklassen-Beschreibung bei super zu unserer neuen Klas-senbeschreibung bei self kopieren:

const size_t offset = offsetof(struct Class, ctor);...memcpy((char *) self + offset, (char *) self -> super

+ offset, sizeOf(self -> super) - offset);

Wir nehmen an, daß der Konstruktor die erste Methode in struct Class ist, undbenützen den ANSI-C Makro offsetof(), um auszurechnen, wo unsere Kopie begin-nen muß. Glücklicherweise ist die Klassenbeschreibung bei super letztlich einObject und hat damit sizeOf() geerbt, so daß wir ausrechnen können, wie vieleBytes wir kopieren müssen.

Die Lösung ist zwar nicht absolut sicher, aber sie erscheint als guter Kompro-miß. Natürlich könnten wir die ganze Fläche bei super kopieren und den neuen Na-men etc. anschließend speichern, aber wir müßten immer noch die struct Objectam Anfang der neuen Klassenbeschreibung retten, denn new() hat dort schon denZeiger gespeichert, der von der neuen Klassenbeschreibung auf ihre eigene Klas-senbeschreibung zeigt.

Der Rest des Class-Konstruktors muß die Methoden ersetzen, die in der Argu-mentliste von new() angegeben wurden. ANSI-C erlaubt nicht, daß wir Funktions-zeiger von und an void * zuweisen, also müssen wir explizit umwandeln:

{typedef void (* voidf) (); /* allg. Funktionszeiger */voidf selector;va_list ap = * app;

while ((selector = va_arg(ap, voidf))){ voidf method = va_arg(ap, voidf);

if (selector == (voidf) ctor)* (voidf *) & self -> ctor = method;

Page 79: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

69___________________________________________________________________________6.7 Initialisierung

else if (selector == (voidf) dtor)* (voidf *) & self -> dtor = method;

else if (selector == (voidf) differ)* (voidf *) & self -> differ = method;

else if (selector == (voidf) puto)* (voidf *) & self -> puto = method;

}

return self;}}

Wie wir im Abschnitt 6.10 noch sehen, machen wir diesen Teil der Argumentlisteam besten allen Klassenkonstruktoren zugänglich, damit die Selektor/Methode-Paa-re in beliebiger Reihenfolge angegeben werden können. Wir erreichen dies da-durch, daß wir * app nicht weiter inkrementieren. Statt dessen übergeben wir eineKopie ap dieses Werts an va_arg().

Wenn wir die Methoden auf diese Weise speichern, handeln wir uns ein paarKonsequenzen ein: Wenn kein Klassenkonstruktor einen bestimmten Selektor ak-zeptiert, wird ein Selektor/Methode-Paar stillschweigend ignoriert, aber es wird we-nigstens nicht in eine Klassenbeschreibung eingetragen, zu der es nicht gehört.Wenn eine Methode nicht den richtigen Typ hat, entdeckt der ANSI-C Compiler denFehler nicht, denn die variable Parameterliste und unsere Umwandlungen machenTypprüfungen unmöglich. Wir verlassen uns hier darauf, daß der Programmiererdafür sorgt, daß Selektor und Methode zusammenpassen, aber sie müssen paar-weise angegeben werden, und das sollte zu einigermaßen plausiblen Argumentli-sten führen.

6.7 InitialisierungNormalerweise erhalten wir eine Klassenbeschreibung, indem wir new() mit einerMetaklassen-Beschreibung aufrufen. Für Class und Object würden wir folgendeAufrufe verwenden:

const void * Object = new(Class,"Object", Object, sizeof(struct Object),ctor, Object_ctor,dtor, Object_dtor,differ, Object_differ,puto, Object_puto,0);

const void * Class = new(Class,"Class", Object, sizeof(struct Class),ctor, Class_ctor,dtor, Class_dtor,0);

Leider müßte sich jeder der beiden Aufrufe darauf verlassen, daß der andere bereitserfolgreich abgeschlossen wurde. Deshalb müssen bei der Implementierung vonClass und Object in Object.c die Klassenbeschreibungen statisch initialisiert wer-den. Dies ist der einzige Ort, wo wir eine struct Class explizit initialisieren:

Page 80: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

70___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

static const struct Class object [] = {{ { object + 1 },

"Object", object, sizeof(struct Object),Object_ctor, Object_dtor, Object_differ, Object_puto

},{ { object + 1 },

"Class", object, sizeof(struct Class),Class_ctor, Class_dtor, Object_differ, Object_puto

}};

const void * Object = object;const void * Class = object + 1;

Ein Vektorname ist die Adresse des ersten Vektorelements und kann bereits zur In-itialisierung von Komponenten der Elemente verwendet werden. Wir klammerndiese Initialisierungen vollständig, damit struct Object später leicht geändert wer-den kann.

6.8 SelektorenDie Aufgabe einer Selektor-Funktion ist seit dem zweiten Kapitel unverändert: EinArgument _self ist das Objekt, über das die dynamische Bindung erfolgt. Wir stel-len sicher, daß das Objekt selbst und die gewünschte Methode für diese Art vonObjekt existieren. Dann rufen wir die Methode auf und übergeben alle Argumente.Die Methode selbst kann deshalb davon ausgehen, daß _self die richtige Art vonObjekt für sie ist. Zum Schluß liefern wir bei Bedarf den Resultatwert der Methodeals Resultat des Selektors.

Jede dynamisch gebundene Methode benötigt einen Selektor. Bisher habenwir die Aufrufe von Konstruktor und Destruktor hinter new() und delete() versteckt,aber wir benötigen trotzdem die Funktionsnamen ctor und dtor für die Selektor/Me-thode-Paare, die an den Class-Konstruktor übergeben werden. Wir sehen im Kapi-tel 11, daß es durchaus sinnvoll sein kann, new() und delete() dynamisch zu bin-den, deshalb sollten wir nicht etwa ihre Namen an Stelle von ctor und dtor verwen-den.

Wir haben die gemeinsame Oberklasse Object für alle unsere Klassen ein-geführt, und sie besitzt die Methode classOf(), die die Implementierung von Selek-tor-Funktionen vereinfacht. classOf() untersucht ein Objekt und liefert einen vonNull verschiedenen Zeiger auf seine Klassenbeschreibung. Damit können wirdelete() und dtor() folgendermaßen implementieren:

void delete (void * _self){

if (_self)free(dtor(_self));

}

Page 81: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

71___________________________________________________________________________6.9 Oberklassen-Selektoren

void * dtor (void * _self){ const struct Class * class = classOf(_self);

assert(class -> dtor);return class -> dtor(_self);

}

new() muß sehr sorgfältig implementiert werden, aber die Funktion arbeitet ganzähnlich:

void * new (const void * _class, ...){ const struct Class * class = _class;

struct Object * object;va_list ap;

assert(class && class -> size);object = calloc(1, class -> size);assert(object);object -> class = class;va_start(ap, _class);object = ctor(object, & ap);va_end(ap);return object;

}

Wir verifizieren die Klassenbeschreibung und stellen sicher, daß wir ein mit Nullgefülltes Objekt erzeugen können. Dann initialisieren wir die Klassenbeschreibungim Objekt. Anschließend können wir den normalen Selektor ctor() den Konstruktorfinden und ausführen lassen:

void * ctor (void * _self, va_list * app){ const struct Class * class = classOf(_self);

assert(class -> ctor);return class -> ctor(_self, app);

}

Vielleicht wird ein bißchen zuviel überprüft, aber wir haben eine einheitliche und ro-buste Lösung.

6.9 Oberklassen-SelektorenBevor ein Unterklassen-Konstruktor seine eigenen Initialisierungen vornimmt, mußer den Oberklassen-Konstruktor aufrufen. Analog muß der Unterklassen-Destruktorseinen Oberklassen-Destruktor aufrufen, nachdem er seine eigenen Ressourcenfreigegeben hat. Wenn wir schon Selektor-Funktionen implementieren, sollten wirauch Selektoren für diese Oberklassen-Aufrufe realisieren:

void * super_ctor (const void * _class,void * _self, va_list * app)

{ const struct Class * superclass = super(_class);

assert(_self && superclass -> ctor);return superclass -> ctor(_self, app);

}

Page 82: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

72___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

void * super_dtor (const void * _class, void * _self){ const struct Class * superclass = super(_class);

assert(_self && superclass -> dtor);return superclass -> dtor(_self);

}

Diese Selektoren sollten nur in der Implementierung einer Unterklasse verwendetwerden, deshalb deklarieren wir sie in der Repräsentierungsdatei und nicht in derSchnittstellendatei. Um sicherzugehen implementieren wir Oberklassen-Selektorenfür alle dynamisch gebundenen Methoden, das heißt, zu jedem Selektor gibt es denzugehörigen Oberklassen-Selektor. So kann jede dynamisch gebundene Methodeleicht ihre Oberklassen-Methode aufrufen.

Hier ist tatsächlich eine subtile Falle versteckt. Betrachten wir, wie eine Metho-de in einer beliebigen Klasse X ihre Oberklassen-Methode aufrufen würde. Hier istdie korrekte Version:

static void * X_method (void * _self, va_list * app){ void * p = super_method(X, _self, app);

...

Wenn wir die vorher gezeigten Oberklassen-Selektoren betrachten, sehen wir, daßsuper_method() in diesem Fall

super(X) -> method(_self, app);

aufruft, das heißt, die Methode in der Oberklasse der Klasse X, für die wir geradeX_method() definiert haben. Die gleiche Methode wird auch dann erreicht, wenneine Unterklasse Y die Methode X_method() geerbt hat, denn die Implementierungist von jeder zukünftigen Vererbung unabhängig.

Der folgende Code für X_method() sieht vielleicht plausibler aus, aber er funk-tioniert nicht mehr, wenn die Methode vererbt wird:

static void * X_method (void * _self, va_list * app){ void * p = /* FALSCH */

super_method(classOf(_self), _self, app);...

Der Oberklassen-Selektor produziert jetzt

super(classOf(_self)) -> method(_self, app);

Wenn _self in Klasse X ist, erreichen wir die gleiche Methode wie vorher. Wennaber _self in einer Unterklasse Y von X ist, erhalten wir

super(Y) -> method(_self, app);

und das ist noch immer X_method(), das heißt, statt daß wir eine Oberklassen-Me-thode aufrufen, bleiben wir in einer Folge von rekursiven Aufrufen stecken!

6.10 Eine neue Metaklasse — PointClassObject und Class bilden die Wurzel unserer Klassenhierarchie. Jede Klasse ist eineUnterklasse von Object und erbt ihre Methoden, jede Metaklasse ist eine Unter-klasse von Class und kooperiert mit ihrem Konstruktor. Any im Abschnitt 6.4 hat

Page 83: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

73___________________________________________________________________________6.10 Eine neue Metaklasse — ‘‘PointClass’’

gezeigt, wie eine einfache Unterklasse konstruiert wird, indem man dynamischeMethoden ihrer Oberklasse ersetzt und vielleicht neue statisch gebundene Metho-den einführt.

Wir beschäftigen uns jetzt mit Klassen mit mehr Funktionalität. Als Beispielverbinden wir Point und Circle mit unserer Klassenhierarchie. Diese Klassen habeneine neue dynamisch gebundene Methode draw(), also benötigen wir eine neueMetaklasse, um die dynamische Bindung zu speichern. Hier ist die Schnittstellen-datei Point.h:

#include "Object.h"

extern const void * Point; /* new(Point, x, y); */

void draw (const void * self);void move (void * point, int dx, int dy);

extern const void * PointClass; /* fuegt draw hinzu */

In der Definitionsdatei der Unterklasse fügen wir immer die Definitionsdatei derOberklasse ein, und wir definieren einen Zeiger auf die Klassenbeschreibung undauf die Metaklassen-Beschreibung, falls es eine neue gibt. Nachdem wir jetzt Me-taklassen eingeführt haben, können wir den Selektor für eine dynamisch gebundeneMethode endlich dort deklarieren, wo er hingehört: in der gleichen Schnittstellenda-tei wie der Zeiger auf die Metaklasse.

Die Repräsentierungsdatei Point.r enthält struct Point mit den Zugriffsmakroswie vorher, und sie enthält die Deklarationen der Oberklassen-Selektoren samt derStruktur der Metaklasse:

#include "Object.r"

struct Point { const struct Object _; /* Point : Object */int x, y; /* Koordinaten */

};

#define x(p) (((const struct Point *)(p)) -> x)#define y(p) (((const struct Point *)(p)) -> y)

void super_draw (const void * class, const void * self);

struct PointClass {const struct Class _; /* PointClass : Class */void (* draw) (const void * self);

};

Die Implementierung in Point.c enthält move(), Point_draw(), draw() undsuper_draw(). Diese Methoden werden so geschrieben wie vorher; wir haben dieTechnik für den Oberklassen-Selektor im vorigen Abschnitt gesehen. Der Konstruk-tor muß den Oberklassen-Konstruktor aufrufen:

static void * Point_ctor (void * _self, va_list * app){ struct Point * self = super_ctor(Point, _self, app);

self -> x = va_arg(* app, int);self -> y = va_arg(* app, int);return self;

}

Page 84: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

74___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

Eine neue Idee in dieser Datei ist der Konstruktor für die Metaklasse. Er ruftseinen Oberklassen-Konstruktor auf, um die Vererbung durchzuführen, und benütztdann sinngemäß die gleiche Schleife wie Class_ctor(), um die neue dynamisch ge-bundene Methode draw() zu ersetzen:

static void * PointClass_ctor (void * _self, va_list * app){ struct PointClass * self

= super_ctor(PointClass, _self, app);typedef void (* voidf) ();voidf selector;va_list ap = * app;

while ((selector = va_arg(ap, voidf))){ voidf method = va_arg(ap, voidf);

if (selector == (voidf) draw)* (voidf *) & self -> draw = method;

}

return self;}

Wir benutzen die Selektor/Methode-Paare in der Argumentliste gemeinsam mit demOberklassen-Konstruktor: ap verwendet das, was Class_ctor() in * app hinterlegthat, und startet die Schleife von dort.

Mit diesem Konstruktor können wir jetzt die neuen Klassenbeschreibungen dy-namisch erzeugen und initialisieren: PointClass wird mit Class erzeugt, und dannwird Point mit der Klassenbeschreibung PointClass konstruiert:

void initPoint (void){

if (! PointClass)PointClass = new(Class, "PointClass",

Class, sizeof(struct PointClass),ctor, PointClass_ctor,0);

if (! Point)Point = new(PointClass, "Point",

Object, sizeof(struct Point),ctor, Point_ctor,draw, Point_draw,0);

}

Die Initialisierungen sind einfach: Wir geben die Klassennamen, Vererbungsverhält-nisse und die Größe der Objekt-Strukturen an, und dann fügen wir Selektor/Metho-de-Paare für alle dynamisch gebundenen Methoden hinzu, die in der Datei definiertwurden. Null schließt jede Argumentliste ab.

Im neunten Kapitel werden wir diese Initialisierung automatisieren. Im Momentdeklarieren wir initPoint() als Teil der Schnittstelle in Point.h, und diese Funktionmuß definitiv aufgerufen werden, bevor wir Punkte oder Unterklassen erzeugenkönnen. Die Funktion ist so codiert, daß sie mehrfach aufgerufen werden kann —sie erzeugt genau einmal je eine Klassenbeschreibung PointClass und Point.

Page 85: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

75___________________________________________________________________________6.11 Zusammenfassung

Wenn wir initPoint() in main() aufrufen, können wir das Testprogramm pointsaus Abschnitt 4.1 wiederverwenden, und wir erhalten die gleiche Ausgabe:

$ points p"." at 1,2"." at 11,22

Circle ist eine Unterklasse von Point, die im vierten Kapitel vorgestellt wurde.Wenn wir diese Klasse zu unserer Hierarchie hinzufügen, können wir den häßlichenCode aus dem Konstruktor im Abschnitt 4.7 entfernen:

static void * Circle_ctor (void * _self, va_list * app){ struct Circle * self = super_ctor(Circle, _self, app);

self -> rad = va_arg(* app, int);return self;

}

Natürlich müssen wir eine Initialisierungsfunktion initCircle() einführen, die vonmain() aufgerufen werden muß, bevor Kreise erzeugt werden können:

void initCircle (void){

if (! Circle){ initPoint();

Circle = new(PointClass, "Circle",Point, sizeof(struct Circle),ctor, Circle_ctor,draw, Circle_draw,0);

}}

Da Circle von Point abhängt, rufen wir initPoint() auf, bevor wir Circle initialisieren.Alle diese Funktionen tun ihre eigentliche Arbeit nur einmal, und wir können sie inbeliebiger Reihenfolge aufrufen, wenn wir die Abhängigkeiten in den Funktionenselbst berücksichtigen.

6.11 ZusammenfassungObjekte zeigen auf ihre Klassenbeschreibungen, die vor allem Zeiger auf die dyna-misch gebundenen Methoden enthalten. Klassenbeschreibungen mit der gleichenFolge von Methodenzeigern bilden eine Metaklasse — auch Klassenbeschreibungensind Objekte. Eine Metaklasse hat selbst wieder eine Klassenbeschreibung.

Die Dinge bleiben endlich, da wir mit einer trivialen Klasse Object und einer er-sten Metaklasse Class anfangen, die Object als Oberklasse besitzt. Wenn die glei-che Menge von Methoden — Konstruktor, Destruktor, Vergleich und Anzeige — aufObjekte und Klassenbeschreibungen angewendet werden kann, dann beschreibt dieMetaklassen-Beschreibung Class nicht nur die Klassenbeschreibung Object, son-dern auch sich selbst.

Ein Metaklassen-Konstruktor füllt eine Klassenbeschreibung und implementiertdeshalb binäre Vererbung, der Destruktor liefert einen Nullzeiger, damit eine Klas-

Page 86: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

76___________________________________________________________________________6 Klassenhierarchie — Pflege vereinfachen

senbeschreibung nicht zerstört werden kann, die Anzeigefunktion könnte auch Me-thodenzeiger darstellen etc. Zwei Klassenbeschreibungen sind genau dann gleich,wenn ihre Anfangsadressen gleich sind.

Wenn wir neue dynamisch gebundene Methoden wie draw() einführen, müs-sen wir eine neue Metaklasse beginnen, denn ihr Konstruktor muß diese Metho-denadresse in eine Klassenbeschreibung eintragen. Die Metaklassen-Beschreibungverwendet immer struct Class und wird deshalb mit folgender Art von Aufruf er-zeugt:

PointClass = new(Class, ...ctor, PointClass_ctor,0);

Wenn die Metaklassen-Beschreibung existiert, können wir Klassenbeschreibungenin dieser Metaklasse erzeugen und jeweils die neue Methode eintragen:

Point = new(PointClass, ...draw, Point_draw,...0);

Diese zwei Aufrufe müssen genau einmal ausgeführt werden, bevor in der neuenKlasse Objekte erzeugt werden können. Alle Metaklassen-Konstruktoren könneneinheitlich so geschrieben werden, daß die Selektor/Methode-Paare in beliebigerReihenfolge angegeben werden können. Mehr Klassen können in der gleichen Me-taklasse erzeugt werden, indem wir einfach new() mit der Metaklassen-Beschrei-bung aufrufen.

Selektoren werden ebenfalls einheitlich programmiert. Es ist eine gute Idee,wenn Konstruktoren und Destruktoren grundsätzlich Aufrufe entlang der Oberklas-sen-Kette schicken. Um die Codierung zu vereinfachen, programmieren wir auchOberklassen-Selektoren mit den gleichen Argumenten wie die Selektoren. Ein zu-sätzlicher erster Parameter muß angegeben werden, und zwar die Klasse, für diedie Methode definiert wurde, in der der Oberklassen-Selektor aufgerufen wird.Auch die Oberklassen-Selektoren werden nach einem völlig einheitlichen Mustergeschrieben.

Eine kohärente Strategie für die Überprüfung von Parametern macht die Imple-mentierungen kleiner und robuster: Selektoren verifizieren das Objekt, seine Klasseund die Existenz einer Methode; Oberklassen-Selektoren sollten außerdem dasneue Klassenargument überprüfen; eine dynamisch gebundene Methode wird nurüber einen Selektor aufgerufen, das heißt, sie muß ihr Objekt nicht mehr überprü-fen. Eine statisch gebundene Methode unterscheidet sich nicht von einem Selek-tor: Sie muß ihr Objekt prüfen.

Betrachten wir die Bedeutung von zwei grundsätzlichen Komponenten von Ob-jekten und unsere Konventionen zur Entwicklung von Namen. Jede Klasse hatschließlich Object als Oberklasse. Ausgehend von einem Zeiger p auf ein Objekt inirgendeiner beliebigen Unterklasse von Object, zeigt die Komponente p−>class aufdie Klassenbeschreibung des Objekts. Nehmen wir an, daß der Zeiger C auf diegleiche Klassenbeschreibung zeigt, und daß C der Klassenname ist, der in der

Page 87: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

77___________________________________________________________________________6.11 Zusammenfassung

Schnittstellendatei C.h vereinbart wurde. Dann wird das Objekt, auf das p zeigt, mitstruct C repräsentiert. Dies erklärt, warum im Abschnitt 6.3 Class−>class aufClass selbst zeigen muß: Das Objekt, auf das Class zeigt, wird mit einer structClass repräsentiert.

Jede Klassenbeschreibung muß mit struct Class beginnen, damit wir dort zumBeispiel den Klassennamen und einen Zeiger auf die Beschreibung der Oberklassespeichern können. Lassen wir jetzt C auf eine Klassenbeschreibung zeigen undC−>super auf die gleiche Klassenbeschreibung wie der Zeiger S, der in der Schnitt-stellendatei S.h deklariert wird, das heißt, S ist die Oberklasse von C. In diesem Fallmuß struct C mit struct S beginnen. Dies erklärt, warum im Abschnitt 6.3Class−>super auf Object zeigt: Wir beschlossen, daß struct Class mit structObject beginnt.

Die einzige Ausnahme zu dieser Regel ist, daß Object−>super den Wert Objecthat, wobei in Abschnitt 6.3 erklärt wurde, daß dies eine relativ willkürliche Entschei-dung war.

Page 88: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 89: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

79___________________________________________________________________________

7Der ooc Präprozessor

Codierstandards durchsetzen

Wenn wir das letzte Kapitel betrachten, haben wir offenbar das große Problem,Klassen systematisch zu pflegen, dadurch gelöst, daß wir ein anderes großes Pro-blem erfunden haben: Jetzt haben wir eine Vielzahl von Konventionen, wie be-stimmte Funktionen geschrieben werden müssen (insbesondere der Metaklassen-Konstruktor) und welche zusätzlichen Funktionen nötig sind (Selektoren, Oberklas-sen-Selektoren und Initialisierungen). Außerdem haben wir Regeln zur defensivenProgrammierung, das heißt, zur Prüfung von Argumenten, aber die Regeln sindnicht einheitlich: Wir sollten übervorsichtig in Selektoren und statisch gebundenenMethoden sein, aber wir dürfen ein bißchen vertrauensvoller in dynamisch gebun-denen Methoden vorgehen. Wenn wir etwa später diese Regeln ändern, müssenwir wahrscheinlich ziemlich viel reichlich standardisierten Programmtext korrigieren— eine langweilige und fehleranfällige Tätigkeit.

In diesem Kapitel entwerfen wir einen Präprozessor ooc, der uns hilft, die Kon-ventionen aus den bisherigen Kapiteln einzuhalten. Der Präprozessor ist einfach ge-nug, daß er in wenigen Tagen mit awk [AWK88] implementiert werden kann, und ererlaubt uns, Codierkonventionen zu folgen und sie später auch abzuwandeln. DieImplementierung von ooc wird im Anhang B erklärt, die Benutzung ist auf einer Ma-nualseite im Anhang C beschrieben, und die kompletten Quellen sind Teil der Quel-len zu diesem Buch.

ooc hat nicht zum Ziel, eine neue Programmiersprache einzuführen — wir arbei-ten nach wie vor nur mit ANSI-C, und die Ausgabe von ooc könnte genausogut vonHand geschrieben werden. Es ist nur zweckmäßig, reine Routineaufgaben durchgeeignete Werkzeuge erledigen zu lassen. Wenn Codierstandards dazu führen, daßTeile eines Programms mechanisch erzeugt werden können, dann sollte man dasauch tun.

7.1 Point nochmals betrachtetWir wollen einen Präprozessor ooc konstruieren, der uns hilft, unsere Klassen zupflegen und dabei Codierstandards einzuhalten. Am besten entwirft man einen sol-chen Präprozessor, indem man eine typische, existierende Klasse als Beispiel ver-wendet und untersucht, wie man den Implementierungsaufwand für die Klasse re-duzieren kann, wenn man vernünftige Annahmen darüber macht, was ein Präpro-zessor tun kann. Kurz, wir wollen eine Zeitlang ‘‘Präprozessor spielen’’.

Point aus Kapitel 4 und Abschnitt 6.10 ist ein gutes Beispiel: Es ist nicht dieWurzelklasse unseres Systems, wir benötigen eine neue Metaklasse, und wir ha-ben wenige, aber typische Methoden. Ab jetzt verwenden wir kursive Schrift undbezeichnen die Klasse als Point, um zu betonen, daß es hier nur um ein Modellgeht, das unser Präprozessor bearbeiten soll.

Page 90: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

80___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

Wir beginnen mit einer Klassenbeschreibung, die sich mehr oder weniger selbsterklärt, die wir leicht verstehen können und die einfach genug ist, daß sie ein awk -basierter, also zeilenorientierter Präprozessor analysieren kann:

% PointClass: Class Point: Object { // Kopfint x; // Objekt-Komponentenint y;

% // statisch gebundenvoid move (_self, int dx, int dy);

%- // dynamisch gebundenvoid draw (const _self);

%}In dieser Klassenbeschreibung wird Fettdruck für Symbole verwendet, die ooc er-kennt; in normaler Druckerschrift erscheinen Dinge, die der Präprozessor hier liestund an anderen Stellen wieder einfügt. Kommentare beginnen mit // und reichenbis zum Ende einer Zeile; Zeilen können mit einem Gegenschrägstrich \ fortgesetztwerden.

Hier beschreiben wir eine neue Klasse Point als Unterklasse von Object. DiePoint-Objekte haben neue Komponenten x und y, jeweils mit Typ int. Es gibt einestatisch gebundene Methode move() , die ihr Objekt mit Hilfe der anderen Parame-ter ändern kann. Wir führen auch eine neue dynamisch gebundene Methode draw()ein, deshalb müssen wir eine neue Metaklasse PointClass vereinbaren, die die Me-ta-Oberklasse Class erweitert. Das Objekt-Argument von draw() ist mit const ver-einbart, das heißt, draw() kann dieses Argument nicht ändern.

Wenn wir keine neuen dynamisch gebundenen Methoden haben, ist die Be-schreibung noch einfacher. Betrachten wir Circle als typisches Beispiel:

% PointClass Circle: Point { // Kopfint rad; // Objekt-Komponente

%} // keine neuen MethodenDiese einfachen, zeilenorientierten Beschreibungen enthalten schon genügend

Information, daß wir die Schnittstellendateien komplett erzeugen können. Hier istein Muster, aus dem hervorgeht, wie ooc die Datei Point.h generieren würde:

#ifndef Point_h#define Point_h

#include "Object.h"

extern const void * Point;

für alle Methoden in %void move (void * self, int dx, int dy);

falls neue Metaklasseextern const void * PointClass;

für alle Methoden in %-void draw (const void * self);

Page 91: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

81___________________________________________________________________________7.1 ‘‘Point’’ nochmals betrachtet

void initPoint (void);

#endifFettdruck markiert Teile des Musters, die in allen Schnittstellendateien gleich sind.Die normale Druckerschrift bezeichnet Information, die ooc in der Klassenbeschrei-bung liest und in die Schnittstellendatei einfügt. Parameterlisten werden ein biß-chen manipuliert: _self oder const _self werden in geeignete Zeiger verwandelt, al-le anderen Parameter können unverändert kopiert werden.

Teile des Musters verwenden wir mehrfach, zum Beispiel für alle Methoden miteiner bestimmten Bindung oder für alle Parameter einer Methode. Andere Teile desMusters hängen von Bedingungen ab, wie etwa, daß eine neue Metaklasse defi-niert wird. Dies ist durch kursive Schrift und Einrücken angedeutet.

Die Klassenbeschreibung enthält auch genügend Information, um die Repräsen-tierungsdatei zu erzeugen. Hier ist ein Muster, um Point.r zu generieren:

#ifndef Point_r#define Point_r

#include "Object.r"

struct Point { const struct Object _;für alle Komponenten

int x;int y;

};

falls neue Metaklassestruct PointClass { const struct Class _;

für alle Methoden in %-void (* draw) (const void * self);

};

für alle Methoden in %-void super_draw (const void * class, const void * self);

#endifDie ursprüngliche Datei steht im Abschnitt 6.10. Sie enthält Definitionen für

zwei Zugriffsmakros x() und y(). Damit ooc sie in die Repräsentierungsdatei übertra-gen kann, vereinbaren wir, daß eine Klassenbeschreibungsdatei außer der Klassen-beschreibung selbst noch zusätzliche Zeilen enthalten kann. Diese Zeilen werden ineine Schnittstellendatei kopiert oder auch in eine Repräsentierungsdatei, wenn ih-nen die Zeile %prot vorausgeht. prot bezieht sich auf geschützte Information —auf solche Zeilen haben die Implementierungen einer Klasse und ihrer UnterklassenZugriff, aber keine Anwendung, die diese Klassen benützt.

Die Klassenbeschreibung enthält genügend Information, so daß ooc auch einenwesentlichen Teil der Implementierung generieren kann. Betrachten wir die ver-schiedenen Teile von Point.c als Beispiel:

Page 92: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

82___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

#include "Point.h" // include#include "Point.r"

Zuerst fügt die Quelldatei die Schnittstellen- und Repräsentierungsdateien ein.// Methodenkopf

void move (void * _self, int dx, int dy) {für alle Parameter // Objekte importieren

falls Parameter vom Typ Pointstruct Point * self = _self;

für alle Parameter // Objekte pruefenfalls Parameter ein Objekt ist

assert(_self);

... // MethodenkoerperBei statisch gebundenen Methoden können wir kontrollieren, daß sie für die Klasseerlaubt sind, bevor wir den Methodenkopf generieren. In einer Schleife über die Pa-rameter können wir lokale Variablen für alle die Parameter initialisieren, die sich aufObjekte der Klasse beziehen, zu der die Methode gehört, und wir können die Me-thode gegen Nullzeiger schützen.

// Methodenkopfstatic void Point_draw (const void * _self) {

für alle Parameter // Objekte importierenfalls Parameter vom Typ Point

const struct Point * self = _self;

... // MethodenkoerperFür dynamisch gebundene Methoden können wir ebenfalls kontrollieren, ob sie fürdie Klasse erlaubt sind, die Köpfe erzeugen und Objekte importieren. Das Musterkann ein bißchen anders sein, denn der Selektor sollte ja schon untersucht haben,daß die Objekte zu den richtigen Klassen gehören.

Es gibt allerdings ein paar Probleme. Als Unterklasse von Object darf unsereKlasse Point eine dynamisch gebundene Methode wie ctor() überschreiben, die zu-erst in Object aufgetaucht ist. Wenn ooc alle Methodenköpfe generieren soll, müs-sen wir die Beschreibungen aller Oberklassen zurück bis zur Wurzel der Klassen-hierarchie lesen. Mit dem Oberklassen-Namen Object in der Klassenbeschreibungfür Point müssen wir deshalb die Klassenbeschreibungsdatei der Oberklasse findenkönnen. Die offensichtliche Lösung besteht darin, die Beschreibung von Object ineiner Datei mit einem daraus abgeleiteten Namen wie Object.d zu speichern.

static void * Point_ctor (void * _self, va_list * app) {...

Ein weiteres Problem hat mit der Tatsache zu tun, daß Point_ctor() den Oberklas-sen-Selektor aufruft und deshalb Parameterobjekte nicht importieren muß, so wiedas für Point_draw() nötig war. Es ist wahrscheinlich eine gute Idee, wenn wir oocjeweils erklären, ob wir Objekte prüfen und importieren wollen oder nicht.

Page 93: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

83___________________________________________________________________________7.1 ‘‘Point’’ nochmals betrachtet

falls neue Metaklassefür alle Methoden in %-

void draw (const void * _self) { // Selektorconst struct PointClass * class = classOf(_self);

assert(class -> draw);class -> draw(_self);

}// Oberklassen-Selektor

void super_draw (const void * class, const void * _self) {const struct PointClass * superclass = super(class);

assert(_self && superclass -> draw);superclass -> draw(_self);

}Wenn die Klassenbeschreibung eine neue Metaklasse vereinbart, können wir dieSelektoren und Oberklassen-Selektoren für alle neuen dynamisch gebundenen Me-thoden vollständig generieren. Wenn wir wollen, können wir in jedem Selektor miteiner Schleife über die Parameter prüfen, daß Nullzeiger nicht als Objekte auftreten.

falls neue Metaklasse// Metaklassen-Konstruktor

static void * PointClass_ctor (void * _self, va_list * app) {{ struct PointClass * self =

super_ctor(PointClass, _self, app);typedef void (* voidf) ();voidf selector;va_list ap = * app;

while ((selector = va_arg(ap, voidf))){ voidf method = va_arg(ap, voidf);

für alle Methoden in %-if (selector == (voidf) draw){ * (voidf *) & self -> draw = method;

continue;}

}return self;

}Mit einer Schleife über die Methodendefinitionen in der Klassenbeschreibung kön-nen wir den Metaklassen-Konstruktor vollständig generieren. Für den Kopf dieserMethode müssen wir allerdings ooc irgendwie erklären, wie ctor() vereinbart ist —für ein anderes Projekt könnten wir uns andere Konventionen für Konstruktoren aus-denken.

const void * Point; // Klassenbeschreibungenfalls neue Metaklasse

const void * PointClass;

Page 94: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

84___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

void initPoint (void) // Initialisierung{

falls neue Metaklasseif (! PointClass)

PointClass = new(Class, "PointClass",Class, sizeof(struct PointClass),ctor, PointClass_ctor,(void *) 0);

if (! Point)Point = new(PointClass, "Point",

Object, sizeof(struct Point),für alle ersetzten Methoden

ctor, Point_ctor,draw, Point_draw,

(void *) 0);}

Die Initialisierungsfunktion kann mit einer Schleife über alle dynamisch gebundenenMethoden generiert werden, die in der Implementierung ersetzt werden.

7.2 EntwurfAus unserem Experiment, Präprozessor für Point zu spielen, können wir nun einigeSchlußfolgerungen ziehen. Wir begannen mit einer relativ einfachen, zeilenorientier-ten Klassenbeschreibung

Schnittstellenzeilen // beliebig%protRepräsentierungszeilen

% metaclass[: metasuper] class: super { // Kopf... // Objektkomponenten

% // statisch gebundentype name ([const] _self, ...);...

%- // dynamisch gebundentype name ([const] _self, ...);...

%}Die einzige Schwierigkeit besteht darin, daß wir die Parameterlisten zerlegen undTyp und Name in jedem Deklarator trennen müssen. Als billige Lösung verlangenwir, daß const dem Typ vorausgeht, und daß der Typ komplett vor dem Namensteht.* Wir erkennen außerdem folgende Spezialfälle für Deklaratoren:

_self Empfänger in der aktuellen Klasse_name anderes Objekt in der aktuellen Klasseclass @ name Objekt in anderer Klasse

____________________________________________________________________________________________

* Dies kann bei Bedarf immer mit typedef erreicht werden.

Page 95: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

85___________________________________________________________________________7.2 Entwurf

In jedem Fall kann noch const davorstehen. Objekte in der aktuellen Klasse werdenüber lokale Variablen als Zeiger auf ihre Strukturen dereferenziert, wenn sie impor-tiert werden.

Der Dateiname einer Klassenbeschreibung ist der Klassenname, gefolgt von .d,so daß ooc die Beschreibungen der Oberklassen finden kann. Die Metaklasse istdabei kein Problem: Entweder eine Klasse hat die gleiche Metaklasse wie ihreOberklasse, oder eine Klasse hat eine neue Metaklasse, deren Oberklasse dannaber die Metaklasse der Oberklasse dieser Klasse sein muß. So oder so — wennwir die Beschreibung der Oberklasse lesen, haben wir genügend Information, umdie Metaklasse bearbeiten zu können.

Nach Lektüre einer Klassenbeschreibung hat ooc genügend Information, um dieSchnittstellendatei und die Repräsentierungsdatei zu generieren. Es ist eine guteIdee, ein Programm als Filter zu realisieren, das heißt, so daß eine Datei nur durchexplizite Umlenkung der Ausgabe erzeugt wird. Damit erhalten wir folgende typi-sche Aufrufe für unseren Präprozessor:

$ ooc Point -h > Point.h # Schnittstellendatei$ ooc Point -r > Point.r # Repraesentierungsdatei

Die eigentliche Implementierung einer Klasse zu bearbeiten, ist schwieriger. Ir-gendwie muß ooc Methodenkörper finden und erkennen, ob Parameter überprüftund importiert werden sollen. Wenn wir die Methodenkörper ebenfalls in der Klas-senbeschreibungsdatei definieren, halten wir zwar die Teile zusammen, aber wirverursachen wesentlich mehr Rechenaufwand: Bei jedem Aufruf muß ooc Klassen-beschreibungen rückwärts bis zur Wurzelklasse lesen, aber Methodenkörper sinddabei nur in der äußersten Klassenbeschreibungsdatei interessant. Außerdem än-dern sich die Implementierungen wahrscheinlich häufiger als die Schnittstellen.Wenn wir die Methodenkörper zusammen mit den Schnittstellen definieren, wirdmake jedesmal alle Schnittstellendateien neu erzeugen lassen, wenn wir einen Me-thodenkörper ändern, und das dürfte zu unnötig vielen Neuübersetzungen führen.*

Als billige Lösung könnte ooc eine Klassenbeschreibung lesen und das Skelettfür eine Quelldatei generieren, mit allen möglichen Methodenköpfen für eine Klassezusammen mit den neuen Selektoren, falls nötig, und dem Code zur Initialisierung.Wir erreichen das mit einer Schleife von der Klasse aufwärts durch alle Oberklas-sen, um Köpfe und fiktive Körper für alle dynamisch gebundenen Methoden zu ge-nerieren, die wir dabei entdecken. ooc wird dazu folgendermaßen aufgerufen:

$ ooc Point -dc > skeleton # eine Implementierung beginnenSo erhalten wir zwar eine nützliche Ausgangsbasis für eine neue Implementierung,aber sie könnte schwierig zu pflegen sein: skeleton enthält alle möglichen Metho-den. Natürlich sollten wir einfach alle löschen, die wir nicht benötigen. Jede Me-thode ist aber zweimal vorhanden: einmal mit Kopf und fiktivem Körper und einmalin der Argumentliste, mit der die Klassenbeschreibung erzeugt wird. Es ist schwie-rig, aber natürlich unbedingt nötig, daß beide Listen übereinstimmen.____________________________________________________________________________________________

* yacc hat ein ähnliches Problem mit der Definitionsdatei y.tab.h . Die übliche Lösung besteht darin, dieseDatei zu duplizieren, die Kopie nur dann neu zu schreiben, wenn sich wirklich etwas geändert hat, und dieKopie in den Regeln im makefile zu benützen. Siehe [K&P86].

Page 96: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

86___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

ooc soll ein Werkzeug für Pflege und Entwicklung sein. Wenn wir einen Metho-denkopf in einer Klassenbeschreibung ändern, sollte man erwarten können, daß oocdie Änderung in alle Implementierungsdateien überträgt. Wenn wir nur ein neuesskeleton generieren können, müssen wir dann die Änderungen von Hand überneh-men — nicht gerade erfreuliche Aussichten!

Wenn wir Methodenkörper nicht als Teil einer Klassenbeschreibung speichernwollen, und wenn wir verlangen, daß ooc eine existente Quelldatei ändern soll,müssen wir ooc als Präprozessor entwerfen. Hier ist ein typischer Aufruf:

$ ooc Point Point.dc > Point.c # Praeprozessorooc lädt die Klassenbeschreibung für Point, liest dann die Implementierung Point.dcund schreibt eine modifizierte Version dieser Datei als Standard-Ausgabe. Wir kön-nen das sogar mit der vorherigen Technik kombinieren, wenn wir skeleton mitPräprozessor-Anweisungen und nicht bereits als C-Quelle generieren.

7.3 Präprozessor-AnweisungenWas für Präprozessor-Anweisungen soll ooc bearbeiten? Wenn wir unser Experi-ment mit Point im Abschnitt 7.1 ansehen, gibt es drei Bereiche, in denen ooc helfenkann: Für einen Methodennamen kennt ooc den Methodenkopf; für eine Methodekann ooc die Parameterobjekte prüfen und importieren; außerdem können die Se-lektoren, der Metaklassen-Konstruktor und die Initialisierungsfunktion je nach Be-darf generiert werden. Experimentieren wir dazu nochmals mit Point. Die folgendeImplementierungsdatei ist wohl ganz brauchbar:

% move {%casts

self -> x += dx, self -> y += dy;}% Point ctor {

struct Point * self = super_ctor(Point, _self, app);self -> x = va_arg(* app, int);self -> y = va_arg(* app, int);return self;

}% Point draw {%casts

printf("\".\" at %d,%d\n", x(self), y(self));}%init

Fettdruck markiert, worauf ooc reagiert:

% method { Kopf einer statisch gebundenen Methode% class method { Kopf, um dynamisch gebundene Methode zu ersetzen%casts Parameterobjekte importieren%init Selektoren und Initialisierung erzeugen

Page 97: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

87___________________________________________________________________________7.4 Implementierungsstrategie

Bei einer statisch gebundenen Methode kennen wir schon die Klasse, für die sievereinbart wurde. Die Klasse darf aber auch trotzdem angegeben werden — fallswir die Bindung später ändern, müssen wir diese Quelle dann nicht mehr editieren.

Man kann sich fragen, ob man den Benutzer nicht zwingen sollte, den Metho-denkopf vollständig anzugeben, also mit allen Deklaratoren. Damit wäre zwar dieQuelle leichter zu lesen, aber sie ist dann schwerer zu pflegen, wenn man die Ver-einbarung einer Methode ändert. Ein kompletter Methodenkopf ist außerdem et-was schwieriger zu erkennen.

7.4 ImplementierungsstrategieWir wissen jetzt, was ooc tun soll. Wie schreiben wir diesen Präprozessor? AusEffizienzgründen müssen wir vielleicht später doch Werkzeuge wie lex und yacceinsetzen, aber eine erste Implementierung ist wesentlich billiger, wenn wir eineString-Programmiersprache wie awk oder perl verwenden. Wenn das funktioniert,kennen wir die nötigen Algorithmen und Datenstrukturen, und eine effizientere Im-plementierung sollte dann nicht mehr schwerfallen — vielleicht kann diese Arbeitdann auch ein awk -Compiler übernehmen. Vor allem aber können wir mit einer billi-gen ersten Implementierung schnell kontrollieren, ob unsere Idee überhaupt prakti-kabel und bequem ist.

ooc liest die Klassenbeschreibungen und konstruiert eine Datenbasis. Kom-mando-Optionen entscheiden dann, was generiert wird. Wie wir gesehen haben,kann die Generierung jeweils mit einer Art Muster gesteuert werden, das Wörterenthält, die direkt ausgegeben werden, und andere Wörter, die durch Informationaus der Datenbasis ersetzt werden. Unsere Muster enthielten jedoch Schleifen undBedingungen; daraus müßte man schließen, daß ein Muster eine awk -Funktion mitKontrollstrukturen und Variablen ist.

Eine erste Implementierung von ooc funktionierte wirklich so, aber sie warschwer zu ändern. Es geht wesentlich eleganter: Wir verwenden eine einfacheReportsprache mit Textersatz, Schleifen und Bedingungen, in der wir ein Musterformulieren, das dann von ooc interpretiert wird, um die Ausgabe zu generieren.

Die Implementierung wird im Anhang B genauer erklärt. Die Reportsprachewird im ooc-Manual im Anhang C definiert. Sie hat ungefähr 25 Wörter für Texter-satz, zehn Schleifen, zwei Bedingungen sowie die Fähigkeit, daß ein Report als Teileines anderen aufgerufen wird. Als Beispiel ist hier ein Report, der Selektoren ge-neriert:

`{if `newmeta 1`{%-

`result `method ( `{() `const `type `_ `name `}, ) { `n`t const struct `meta * class = classOf(self); `n

`%casts`t assert(class -> `method ); `n`t `{ifnot `result void return `} \

class -> `method ( `{() `name `}, ); `n} `n

`}`}

Page 98: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

88___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

Innerhalb von Reports interessiert sich ooc für alle die Wörter, die mit einem Accentgrave anfangen; Gruppen beginnen mit `{ und enden mit `} und sind entwederSchleifen oder Bedingungen; ein Reportaufruf verwendet `%; alle anderen Wörter,die mit einem Accent grave anfangen, werden durch Information aus der Datenbankersetzt.

`{if nimmt die nächsten beiden Wörter und führt den Rest der Gruppe aus,wenn die Wörter gleich sind. `newmeta wird durch 1 ersetzt, wenn ooc eine Klas-senbeschreibung bearbeitet, die eine neue Metaklasse definiert. Deshalb werdenhier die Selektoren nur generiert, wenn wir eine neue Metaklasse erzeugen.

`{%− ist eine Schleife über die dynamisch gebundenen Methoden in der Klas-senbeschreibung. `method wird durch den aktuellen Methodennamen ersetzt,`result ist der zugehörige Resultattyp.

`{() ist eine Schleife über die Parameter der aktuellen Methode. Die Bedeutungvon `const, `type und `name sollte offensichtlich sein: Es sind die Teile des aktuel-len Parameter-Deklarators. `_ ist ein Unterstrich, wenn der aktuelle Parameter einObjekt der aktuellen Klasse ist. `}, ist ein kleiner Trick: Dafür wird ein Komma aus-gegeben, wenn es noch einen weiteren Parameter gibt, und das Symbol beendettrotzdem eine Schleife, so wie jedes andere Symbol, das mit `} beginnt.

`%casts ruft casts auf, einen anderen Report, der Parameterobjekte importierenmuß. Im Moment sieht dieser Report ungefähr so aus:

% casts // die Anweisung %casts`{() // Import

`{if `_ _ // wenn Parameter in aktueller Klasse...`t `const struct `cast * `name = _ `name ; `n

`}`}n`{if `linkage % // statische Bindung: pruefen

`%checks`}

Die Reports werden aus Dateien eingelesen. Vor jedem Report steht eine Zeile, diemit % beginnt und den Reportnamen enthält. Der Rest des Reports casts sollteziemlich klar sein: `cast steht für den Klassennamen eines Parameterobjekts und`linkage ist die Bindung der aktuellen Methode, das heißt, eines der trennendenSymbole in der Klassenbeschreibung. Wir erzeugen eine lokale Variable, um einenParameter dadurch als Zeiger auf eine Struktur zugänglich zu machen, wenn er einObjekt in der aktuellen Klasse ist. `}n ist ein weiterer Trick: Dafür wird ein Zeilen-trenner ausgegeben, wenn für die dadurch abgeschlossene Gruppe irgendein Textgeneriert wurde.

%casts ist auch dafür verantwortlich, Objekte zu überprüfen, die als Parameteran eine Methode mit statischer Bindung übergeben wurden. Da Selektoren be-kanntlich ein ähnliches Problem haben, benützen wir einen separaten Reportchecks, der auch von einem Report aufgerufen werden kann, der Selektoren gene-riert:

Page 99: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

89___________________________________________________________________________7.5 ‘‘Object’’ nochmals betrachtet

% checks // alle Parameterobjekte pruefen`{()

`{ifnot `cast ` // wenn Parameter ein Objekt ist...`t assert( `name ); `n

`}fi`}n

Bis zum nächsten Kapitel können wir uns wenigstens vor Nullzeigern schützen undassert() aufrufen. Der Test soll nur für Objekte durchgeführt werden: ` wird durchnichts ersetzt, das heißt, wir generieren assert(), wenn wir ein Parameterobjekt ei-ner beliebigen Klasse betrachten.

Zwei Wörter wurden bisher nicht erklärt: `t generiert ein Tabulatorzeichen und`n wird durch einen Zeilentrenner ersetzt. Wir wollen lesbare C-Programme erzeu-gen, deshalb müssen wir genau kontrollieren, wieviel Zwischenraum generiert wird.Reports sollen selbst eingerückt werden, damit die Gruppen sichtbar werden. oocgibt deshalb keinen führenden Zwischenraum aus und erzeugt auch nicht mehr alseine Leerzeile in Folge. Nur mit `t kann das generierte C-Programm eingerückt wer-den und ‘n muß angegeben werden, um die Ausgabe in Zeilen zu zerlegen.*

7.5 Object nochmals betrachtetIm Abschnitt 7.1 sahen wir, daß wir für eine Klasse wie Point die Information überalle Oberklassen aufwärts bis zur Wurzelklasse benötigen. Wie definieren wir alsoObject? Ganz bestimmt sollten wir Object nicht in das awk -Programm von ooc ein-brennen. Die offensichtliche Lösung ist eine Klassenbeschreibungsdatei:

#include <stdarg.h>#include <stddef.h>#include <stdio.h>%prot#include <assert.h>% Class Object {

const Class @ class; // Klassenbeschreibung%

void delete (_self); // Instanz freigebenconst void * classOf (const _self); // Klassesize_t sizeOf (const _self); // Groesse

%-void * ctor (_self, va_list * app); // Konstruktorvoid * dtor (_self); // Destruktorint differ (const _self, const Object @ b); // wahr: !=int puto (const _self, FILE * fp); // ausgeben

%}

____________________________________________________________________________________________

* Experimente mit den Programmen cb und indent zur Verschönerung der C-Quellen führten nicht zu be-friedigenden Resultaten. Die Wörter `t und `n sind ein kleineres Übel, und durch die Überwachung vonführendem Zwischenraum und Folgen von Leerzeilen wird der Reportgenerator nicht wesentlich kompli-zierter.

Page 100: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

90___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

Offenbar ist das ein Spezialfall: Als Wurzelklasse hat Object selbst keine Oberklas-se, und als erste Metaklasse hat Class keine Meta-Oberklasse. Nur eine einzigeKlassenbeschreibung hat diese Eigenschaft, deshalb kann ooc dies als besondereSyntax für die Klassenbeschreibung der `root und `metaroot Klassen erkennen.

Class wirft ein anderes Problem auf: Wir sahen im Abschnitt 7.1, daß neue Me-taklassen direkt mit einer neuen Klasse vereinbart werden können, da sie in der Re-gel nur dynamische Bindungen als neue Komponenten besitzen können. Class istdie erste Metaklasse und hat einige zusätzliche Komponenten:

% Class Class: Object {const char * name; // Klassennameconst Class @ super; // Oberklassesize_t size; // Objektgroesse

%Object @ new (const _self, ...); // Instanz erzeugenconst void * super (const _self); // Oberklasse

%}Es zeigt sich, daß unsere Syntax für Klassenbeschreibungen durchaus ausreicht,um Class zu beschreiben. Es ist ein weiterer Spezialfall für ooc: Dies ist die einzigeKlasse, die sich selbst als Metaklasse hat.

Wenn wir beide Beschreibungen in die gleiche KlassenbeschreibungsdateiObject.d aufnehmen, und wenn wir Object vor Class vereinbaren, endet die Suchenach Klassenbeschreibungen in ooc ganz von selbst. Unsere Datenbasis istvollständig.

Wir könnten die Implementierung von Object und Class von Hand schreiben —es macht wenig Sinn, Code zu ooc hinzuzufügen, der nur für eine einzige Imple-mentierung verwendet wird. Unser Reportgenerator ist aber gut genug, daß wir ihnfür Object anpassen können, und wir erhalten sehr brauchbare Resultate.

Die Schnittstellendateien für Point und Object sind ziemlich ähnlich, nur daß inObject.h keine Oberklassen-Schnittstelle eingefügt und keine Initialisierungsfunktionvereinbart wird. Die zuständige Reportdatei h.rep wird allerdings recht häufig ver-wendet, so daß wir sie vielleicht nicht mit Bedingungen überladen sollten, die nor-malerweise nicht benützt werden. Statt dessen geben wir ooc einfach einen zu-sätzlichen Parameter:

$ ooc -R Object -h > Object.hDieser Parameter sorgt dafür, daß eine besondere Reportdatei h−R.rep geladenwird, die speziell auf die Wurzelklasse zugeschnitten ist. Beide Reportdateien gene-rieren vor allem Prototypen für Methoden, und sie teilen sich deshalb eine weitereReportdatei header.rep, die den Report header enthält, der in beiden Fällen verwen-det wird.

Ebenso haben die Repräsentierungsdateien für Point und Object viel gemein-sam, und wir benützen −R dazu, eine Reportdatei r−R.rep an Stelle von r.rep zu la-den, um die Unterschiede zu berücksichtigen:

$ ooc -R Object -r > Object.r

Page 101: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

91___________________________________________________________________________7.6 Diskussion

Object.r muß keine Oberklassen-Repräsentierung einfügen, und die Metaklassen-Struktur für Class beginnt mit den zusätzlichen Komponenten. Der gemeinsameCode, der Oberklassen-Selektoren und Methoden als Metaklassen-Komponentendeklariert, steht in einer weiteren Reportdatei va.rep .

Schließlich können wir −R und eine weitere Reportdatei c−R.rep an Stelle vonc.rep verwenden, um bei der C-Quelle zu helfen:

$ ooc -R Object Object.dc > Object.cooc fügt #include-Anweisungen hinzu und bearbeitet Methodenköpfe in

Object.dc wie in jeder anderen Quelle. Der einzige Unterschied liegt in der Überset-zung von %init: Wir können ooc immer noch die Selektoren und Oberklassen-Selek-toren generieren lassen, aber wir müssen die im Abschnitt 6.7 gezeigte statischeInitialisierung der Klassenbeschreibungen von Hand codieren.

Man kann sich überlegen, wie der Metaklassen-Konstruktor Class_ctor() ge-schrieben werden soll. Wenn wir das von Hand in Object.dc erledigen, codieren wirdie Schleife zweimal, die die Selektor/Methode-Paare bearbeitet: einmal inObject.dc für Class_ctor() und einmal in der Reportdatei c.rep für alle anderen Klas-sen. Es zeigt sich, daß wir genug Information besitzen, um es in c−R.rep zu erledi-gen. Wenn wir annehmen, daß die ersten paar Argumente für den Konstruktor inder gleichen Reihenfolge angegeben werden wie die Komponenten von Class, kön-nen wir den ganzen Konstruktor generieren und damit den Code der Schleife als Re-port meta-ctor-loop einmal in einer gemeinsamen Reportdatei etc.rep programmie-ren.

7.6 DiskussionObject demonstriert etwas, das gleichzeitig Stärke und Schwäche unserer Imple-mentierungsstrategie für ooc ist: Wir können wählen, wo wir unsere Entscheidun-gen realisieren. Code kann in einer Klassenbeschreibung oder Implementierung an-gegeben, in einen Report verwandelt oder schließlich tief im awk -Programm vergra-ben werden.

Mit der letzten Möglichkeit müssen wir sicher sehr vorsichtig umgehen: oocsollte für mehr als nur ein Projekt verwendet werden können, deshalb sollte keiner-lei Wissen über ein Projekt, wie zum Beispiel spezielle Codiertechniken, in dasawk -Programm eingebaut werden. Das Programm soll Information sammeln undfür Textersatz anbieten, und es muß Präprozessor-Anweisungen mit Reports verbin-den, aber es sollte nichts über Reportinhalte oder Reihenfolge wissen.

Die Reports können von Projekt zu Projekt geändert werden, und sie sind derPlatz, um Codierstandards durchzusetzen. Reports und alle anderen Dateien, wieKlassenbeschreibungen und Implementierungen, werden in Katalogen gesucht, diein einer Environment-Variablen OOCPATH angegeben sind. Damit kann man für ver-schiedene Projekte verschiedene Versionen der Reports einsetzen.

Unsere Lösung für Object zeigt die Flexibilität, die wir durch Ersetzen von Re-ports gewinnen: Wir können gemeinsame Teile der Reports in gemeinsamen Datei-en anordnen, und wir vermeiden, daß wir im Regelfall besondere Ausnahmen

Page 102: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

92___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

berücksichtigen müssen. Code, der nur einmal benützt wird, kann zwar in eineQuelle geschrieben werden, aber er ist fast genauso leicht als Report zu formulie-ren und kann dann von der Reportgenerierung profitieren. Es gibt praktisch keineEntschuldigung dafür, daß Code dupliziert wird.

Im allgemeinen hat Reportgenerierung Vor- und Nachteile. Positiv ist, daß dieEntwicklung einer Klassenhierarchie und spätere Änderungen vereinfacht werden,denn die Reports sind eine einzige, zentrale Stelle, wo Codierstandards realisiertwerden. Wenn wir zum Beispiel Selektor-Aufrufe verfolgen wollen, fügen wir ein-fach eine entsprechende Programmzeile in den Selektor-Körper in der Reportdateiein, und die Ablaufverfolgung wird überall generiert.

Reportgenerierung dauert allerdings länger als einfache Funktionsaufrufe inawk . Ein Präprozessor sollte #line-Anweisungen für den C-Compiler generieren, da-mit sich Fehlermeldungen auf die ursprünglichen Quellzeilen beziehen. Zwar hatooc eine Technik, um die #line-Anweisungen zu generieren, aber durch die Report-generierung sind diese Anweisungen nicht so exakt, wie sie vielleicht sein könnten.Wenn unsere Reports stabil sind, könnten wir vielleicht einen weiteren Präprozes-sor schreiben, der aus den Reports ein awk -Programm generiert?

7.7 Ein Beispiel — List, Queue und StackImplementieren wir ein paar neue Klassen von vornherein mit ooc, damit wir einGefühl für den Aufwand bekommen, den wir jetzt einsparen. Wir beginnen mit ei-ner List, die wir als Ringpuffer mit zwei Endpunkten implementieren, der bei Bedarfdynamisch expandiert wird.

benutzt

begin end

buf

count

dim

begin und end begrenzen den benutzten Teil der Liste, dim ist die maximale Puffer-größe, und count ist die Anzahl Elemente, die im Moment im Puffer gespeichertsind. Mit count können wir einen vollen und einen leeren Puffer leicht unterschei-den. Hier ist die Klassenbeschreibung List.d:

% ListClass: Class List: Object {const void ** buf; // const void * buf [dim]unsigned dim; // aktuelle Pufferlaengeunsigned count; // # Elemente im Pufferunsigned begin; // Index fuer takeFirst, 0..dimunsigned end; // Index fuer addLast, 0..dim

%Object @ addFirst (_self, const Object @ element);Object @ addLast (_self, const Object @ element);

Page 103: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

93___________________________________________________________________________7.7 Ein Beispiel — ‘‘List’’, ‘‘Queue’’ und ‘‘Stack’’

unsigned count (const _self);Object @ lookAt (const _self, unsigned n);Object @ takeFirst (_self);Object @ takeLast (_self);

%- // abstrakt, fuer Queue/StackObject @ add (_self, const Object @ element);Object @ take (_self);

%}Die Implementierung in List.dc ist nicht sehr schwierig. Der Konstruktor legt einenersten Puffer an:

% List ctor {struct List * self = super_ctor(List, _self, app);if (! (self -> dim = va_arg(* app, unsigned)))

self -> dim = MIN;self -> buf = malloc(self -> dim * sizeof * self -> buf);assert(self -> buf);return self;

}Normalerweise gibt der Benutzer die minimale Puffergröße an. Als Voreinstellungdefinieren wir MIN als geeigneten Wert. Der Destruktor eliminiert den Puffer, abernicht die Elemente, die dort noch gespeichert sind:

% List dtor {%casts

free(self -> buf), self -> buf = 0;return super_dtor(List, self);

}addFirst() und addLast() fügen ein Element bei begin oder end ein:

% List addFirst {%casts

if (! self -> count)return add1(self, element);

extend(self);if (self -> begin == 0)

self -> begin = self -> dim;self -> buf[-- self -> begin] = element;return (void *) element;

}% List addLast {%casts

if (! self -> count)return add1(self, element);

extend(self);if (self -> end >= self -> dim)

self -> end = 0;self -> buf[self -> end ++] = element;return (void *) element;

}

Page 104: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

94___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

Beide Methoden verwenden die gleiche Funktion, um ein erstes Element hinzuzufü-gen:

static void * add1 (struct List * self, const void * element){

self -> end = self -> count = 1;return (void *) (self -> buf[self -> begin = 0] = element);

}Die Invarianten sind jedoch verschieden: Wenn count nicht null ist, das heißt,wenn sich Elemente im Puffer befinden, zeigt begin auf ein Element, aber endzeigt auf einen freien Platz, der gefüllt werden soll. Jeder Wert kann gerade hinterden existenten Teil des Puffers zeigen. Der Puffer wird als Ring benützt, deshalbbilden wir die Variablen erst auf den Ringbereich ab, bevor wir auf den Puffer zugrei-fen. Die Funktion extend() ist der schwierige Teil: Wenn kein Platz mehr frei ist,verwenden wir realloc(), um die Puffergröße zu verdoppeln:

static void extend (struct List * self) // noch ein Element{

if (self -> count >= self -> dim){ self -> buf =

realloc(self -> buf, 2 * self -> dim* sizeof * self -> buf);

assert(self -> buf);if (self -> begin && self -> begin != self -> dim){ memcpy(self -> buf + self -> dim + self -> begin,

self -> buf + self -> begin,(self -> dim - self -> begin)

* sizeof * self -> buf);self -> begin += self -> dim;

}else

self -> begin = 0;}++ self -> count;

}realloc() kopiert die Zeiger, die in buf[] gespeichert sind, aber wenn unser Ringnicht am Pufferanfang beginnt, müssen wir memcpy() verwenden, um den Anfangdes Rings an das Ende des neuen Puffers zu verschieben.

Die restlichen Methoden sind viel einfacher. count() ist nur eine Zugriffsfunkti-on für die Komponente count. Die Methode lookAt() benützt einen arithmetischenTrick, um das richtige Element aus dem Ring zu liefern:

% List lookAt {%casts

return (void *) (n >= self -> count ? 0 :self -> buf[(self -> begin + n) % self -> dim]);

}takeFirst() und takeLast() kehren einfach die Invarianten der zugehörigen add-Funktionen um. Hier ist ein Beispiel:

Page 105: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

95___________________________________________________________________________7.7 Ein Beispiel — ‘‘List’’, ‘‘Queue’’ und ‘‘Stack’’

% List takeFirst {%casts

if (! self -> count)return 0;

-- self -> count;if (self -> begin >= self -> dim)

self -> begin = 0;return (void *) self -> buf[self -> begin ++];

}takeLast() bleibt als Übungsaufgabe — ebenso wie alle Selektoren und Initialisie-rungen, die natürlich mit einem einzigen Wort implementiert werden.

List demonstriert, daß wir mit ooc wieder zu den eigentlichen Implementie-rungsproblemen einer Klasse als Datentyp zurückkehren und uns nicht mehr auf dieFeinheiten eines objekt-orientierten Codierstils konzentrieren müssen. Ausgehendvon einer brauchbaren Basisklasse können wir leicht problemspezifischere Klassenkonstruieren. List hat dynamisch gebundene Methoden add() und take() vorgese-hen, damit eine Unterklasse eine Zugriffsdisziplin einführen kann. Stack arbeitet aneinem Ende:

Stack.d% ListClass Stack: List {%}

Stack.dc% Stack add {

return addLast(_self, element);}% Stack take {

return takeLast(_self);}%init

Eine Warteschlange Queue kann von Stack abgeleitet werden und take() ersetzen,oder sie kann als Unterklasse von List beide Methoden definieren. List selbst defi-niert die dynamisch gebundenen Methoden nicht und würde deshalb als abstrakteBasisklasse bezeichnet werden. Unsere Selektoren sind robust genug, daß wir esbestimmt merken, wenn jemand add() oder take() für List und nicht für eine Unter-klasse verwendet. Hier ist ein Testprogramm, das demonstriert, daß wir sogar ein-fache C-Strings an Stelle echter Objekte in einen Stack oder eine Queue eintragenkönnen:

#include "Queue.h"int main (int argc, char ** argv){ void * q;

unsigned n;initQueue();q = new(Queue, 1);

Page 106: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

96___________________________________________________________________________7 Der ‘‘ooc’’ Präprozessor — Codierstandards durchsetzen

while (* ++ argv)switch (** argv) {case ’+’:

add(q, *argv + 1);break;

case ’-’:puts((char *) take(q));break;

default:n = count(q);while (n -- > 0){ const void * p = take(q);

puts(p), add(q, p);}

}return 0;

}Wenn ein Kommandoargument mit + beginnt, wird es in die Warteschlange einge-tragen; bei − wird ein Element entfernt. Jedes andere Argument zeigt den Inhaltder Warteschlange:

$ queue +axel - +is +here . - . - .axelishereisherehere

Ersetzen wir Queue durch Stack, dann sehen wir den Unterschied an der Reihen-folge, in der Einträge entfernt werden:

$ stack +axel - +is +here . - . - .axelisherehereisis

Da ein Stack eine Unterklasse von List ist, können wir seinen Inhalt mit verschiede-nen Techniken nicht-destruktiv ausgeben, zum Beispiel:

n = count(q);while (n -- > 0){ const void * p = takeFirst(q);

puts(p), addLast(q, p);}

Page 107: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

97___________________________________________________________________________7.8 Überlegungen

7.8 ÜberlegungenEine interessante Aufgabe ist, Queue mit Point und Circle zu kombinieren, so daßein rudimentäres Grafikprogramm entsteht, das Zeichnungen wiederholen kann.

Die Reports −r und include können modifiziert werden, um die opaken Struktur-definitionen zu implementieren, die im Abschnitt 4.6 vorgeschlagen wurden.

Die init-Reports können modifiziert werden, um eine Methode zu generieren,die Information über eine Class-Struktur ausgeben kann.

Selektoren und Oberklassen-Selektoren werden durch Reports in etc.rep gene-riert. Sie können modifiziert werden, damit eine Ablaufverfolgung entsteht, oderum mit verschieden strengen Parameterprüfungen zu experimentieren.

Das Shell-Skript ooc und die Module main.awk und report.awk des awk -Programms können so geändert werden, daß ein Argument −x dafür sorgt, daß einReport x.rep geladen, interpretiert und entfernt wird. Mit dieser Änderung könnteein neuer Report flatten.rep alle Methoden zeigen, die für eine Klasse zur Verfügungstehen.

Page 108: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 109: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

99___________________________________________________________________________

8Dynamische TypprüfungDefensiv programmieren

8.1 StrategieWir greifen auf alle Objekte als void * zu. Das vereinfacht zwar die Codierung, aberes ist riskant: Es gibt ernsthaft Ärger, wenn wir in einer Methode das falsche Ob-jekt oder sogar gar kein Objekt manipulieren, oder noch schlimmer, wenn wir eineMethode aus einer Klassenbeschreibung holen wollen, in der sie gar nicht steht.Verfolgen wir einmal einen Aufruf einer dynamisch gebundenen Methode — new()erzeugt einen Kreis und der Selektor draw() wird darauf angewendet:

void * p = new(Circle, 1, 2, 3);draw(p);

Der Selektor glaubt und benützt das Resultat von classOf():void draw (const void * _self) {

const struct PointClass * class = classOf(_self);assert(class -> draw);class -> draw(_self);

}Die ausgewählte Methode glaubt und benützt _self, also den ursprünglichen Zeiger-wert, den new() erzeugt hat:

static void Circle_draw (const void * _self) {const struct Circle * self = _self;printf("circle at %d,%d rad %d\n",

x(self), y(self), self -> rad);}

Auch classOf() glaubt und benützt einen Zeiger. Als kleines Trostpflaster verifiziertdiese Methode wenigstens, daß ihr Resultat kein Nullzeiger ist:

const void * classOf (const void * _self) {const struct Object * self = _self;assert(self);assert(self -> class);return self -> class;

}Im allgemeinen ist jede Zuweisung eines Zeigerwerts mit Typ void * an einen

Zeiger auf irgendeine Struktur höchst verdächtig, und wir sollten überprüfen, ob die-se Zuweisung erlaubt ist. Wir haben unsere Methoden polymorph gemacht, dasheißt, der ANSI-C Compiler kann diese Prüfung nicht für uns erledigen. Wir müsseneine Technik für dynamische Typprüfung erfinden, die eng begrenzt, wieviel Unheildas falsche Objekt oder ein nicht-Objekt anrichten können.

Page 110: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

100___________________________________________________________________________8 Dynamische Typprüfung — Defensiv programmieren

Glücklicherweise wissen unsere void * Werte, worauf sie zeigen: Sie zeigenalle auf Objekte, die von Object erben und folglich die Komponente .class enthal-ten, die auf ihre Klassenbeschreibung zeigt. Jede Klassenbeschreibung ist eindeu-tig, also können wir mit dem Zeigerwert in .class untersuchen, ob ein Objekt zu ei-ner bestimmten Klasse gehört:

int isA (const _self, const Class @ class);int isOf (const _self, const Class @ class);

Wir können zwei neue statisch gebundene Methoden für Object und daher für je-des beliebige Objekt definieren: isA() ist wahr, wenn ein Objekt direkt zu einer be-stimmten Klasse gehört; isOf() ist wahr, wenn ein Objekt von einer bestimmtenKlasse abgeleitet ist. Folgende Axiome gelten:

isA(0, anyClass) immer falschisOf(0, anyClass) immer falschisOf(x, Object) wahr fuer alle Objekte

Es zeigt sich, daß noch eine andere statisch gebundene Methode für Object sogarnoch nützlicher ist:

void * cast (const Class @ class, const _self);Wenn isOf(_self, class) wahr ist, liefert cast() sein Argument _self, andernfalls ter-miniert cast() den aufrufenden Prozeß.

Ab jetzt werden wir cast() an Stelle von assert() als wesentliches Instrumentzur Schadensbegrenzung verwenden. Immer wenn wir nicht sicher sind, könnenwir einen zweifelhaften Zeiger mit cast() prüfen und damit einschränken, was einunerwarteter Wert anrichten kann:

cast(someClass, someObject);Die Funktion wird außerdem verwendet, um Zeiger sicher zu machen, die in eineMethode oder in einen Selektor importiert werden:

struct Circle * self = cast(Circle, _self);Die Parameter von cast() haben die natürliche Reihenfolge für eine Umwandlungs-operation: Die Klasse steht links vor dem Objekt, das umgewandelt werden soll.Die Methode isOf() erwartet dagegen die Parameter in umgekehrter Reihenfolge,denn in einer if-Anweisung würden wir fragen, ob ein Objekt zu einer bestimmtenKlasse gehört — hier schreiben wir Objekt vor Klasse.

Wenn cast() auch _self mit einem const-Attribut akzeptiert, so hat doch dasResultat dieses Attribut nicht mehr, um Fehlermeldungen bei Zuweisungen zu ver-meiden. Der ANSI-C Standard geht hier mit gutem Beispiel voran: bsearch() liefertvoid * als Resultat, einen Zeiger in eine Tabelle, die als const void * übergebenwurde.

8.2 Ein Beispiel — listAls Beispiel dafür, was wir mit isOf() programmieren und wie sicher wir cast() ma-chen können, betrachten wir folgende Modifikation des Testprogramms im Ab-schnitt 7.7:

Page 111: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

101___________________________________________________________________________8.2 Ein Beispiel — ‘‘list’’

#include "Circle.h"#include "List.h"int main (int argc, char ** argv){ void * q;

unsigned n;initList();initCircle();q = new(List, 1);while (* ++ argv)

switch (** argv) {case ’+’:

switch ((* argv)[1]) {case ’c’:

addFirst(q, new(Circle, 1, 2, 3));break;

case ’p’:addFirst(q, new(Point, 4, 5));break;

default:addFirst(q, new(Object));

}break;

case ’-’:puto(takeLast(q), stdout);break;

case ’.’:n = count(q);while (n -- > 0){ const void * p = takeFirst(q);

if (isOf(p, Point))draw(p);

elseputo(p, stdout);

addLast(q, p);}break;

default:if (isdigit(** argv))

addFirst(q, (void *) atoi(* argv));else

addFirst(q, * argv + 1);}

return 0;}

Für Argumente, die mit + beginnen, fügt dieses Programm Kreise, Punkte oder ein-fache Objekte zu einer Liste hinzu. Das Argument − entfernt das letzte Objekt undzeigt es mit puto(). Das Argument . zeigt den aktuellen Inhalt der Liste, wobei wirdraw() verwenden, wenn ein Eintrag von Point abgeleitet ist. Schließlich versu-chen wir absichtlich, Zahlen und andere Zeichenketten als Argumente in die Liste

Page 112: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

102___________________________________________________________________________8 Dynamische Typprüfung — Defensiv programmieren

einzufügen, obgleich das natürlich Probleme gibt, wenn wir diese Dinge entfernen.Hier ist ein Beispiel:

$ list +c +p + - . 1234Circle at 0x122f4Object at 0x12004"." at 4,5Object.c:66: failed assertion `sig == 0’

Wie wir in Abschnitt 8.4 sehen werden, benützt addFirst() die Methode cast(), umsicherzustellen, daß nur Objekte in die Liste gebracht werden. cast() kann so ro-bust programmiert werden, daß man sogar entdeckt, daß eine Zahl oder eine Zei-chenkette als Objekt posieren.

8.3 ImplementierungMit den oben vorgestellten Axiomen sind die Methoden isA() und isOf() sehr ein-fach zu implementieren:

% isA {return _self && classOf(_self) == class;

}% isOf {

if (_self){ const struct Class * myClass = classOf(_self);

if (class != Object)while (myClass != class)

if (myClass != Object)myClass = super(myClass);

elsereturn 0;

return 1;}return 0;

}Eine erste, reichlich naive Implementierung von cast() könnte isOf() verwenden:

% cast {assert(isOf(_self, class));return (void *) _self;

}isOf(), und damit auch cast(), sind falsch für Nullzeiger. isOf() glaubt ohne weitereUntersuchung, daß jeder Zeiger wenigstens auf eine Instanz von Object zeigt, alsosind wir sicher, daß cast(Object, x) nur für Nullzeiger abbricht. Wir werden aber imAbschnitt 8.5 sehen, daß diese Lösung leicht danebengehen kann.

8.4 CodierstandardDie grundsätzliche Idee ist, cast() so oft wie nötig aufzurufen. Wenn eine statischgebundene Methode Objekte in ihrer eigenen Klasse importiert, sollte sie cast() ver-wenden:

Page 113: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

103___________________________________________________________________________8.4 Codierstandard

void move (void * _self, int dx, int dy) {struct Point * self = cast(Point, _self);self -> x += dx, self -> y += dy;

}Wenn eine derartige Methode Objekte aus einer anderen Klasse erhält, kann sie im-mer noch cast() aufrufen, um sicherzustellen, daß die Parameter das sind, was siebehaupten. Wir haben die %casts-Anweisung in ooc eingeführt, um damit den Im-port einer Parameterliste durchzuführen:

% move {%casts

self -> x += dx, self -> y += dy;}

%casts wird mit dem Report casts in etc.rep implementiert, daher kontrollieren wiralle Objekt-Importe, indem wir diesen Report ändern. Die ursprüngliche Fassungwurde im Abschnitt 7.4 vorgestellt, cast() bauen wir folgendermaßen ein:

% casts // implementiert %casts Anweisung`{() // Import

`{if `_ _`t `const struct `cast * `name = ` \

cast( `cast , _ `name ); `n`}fi

`}n`{if `linkage % // nur bei statischer Bindung

`%checks`}fi

Das Wort `_ wird durch einen Unterstrich ersetzt, wenn der aktuelle Parameter miteinem führenden Unterstrich vereinbart wurde, das heißt, wenn er ein Objekt in deraktuellen Klasse ist. Statt einer einfachen Zuweisung rufen wir cast() auf, bevor wirden Zeiger zuweisen.

Die erste Schleife bei Import kümmert sich um alle Objekte in der eigenenKlasse der Methode. Die anderen Objekte werden im Report checks überprüft:

% checks // alle anderen Objekt-Parameter pruefen`{()

`{ifnot `cast ` `{ifnot `_ _`t cast( `cast , `name ); `n

`}fi `}fi`}n

Ursprünglich hat diese Schleife assert() für alle Objekte generiert. Jetzt können wiruns auf die Objekte beschränken, die nicht in der aktuellen Klasse sind. Für sie ru-fen wir cast() auf, um sicherzustellen, daß sie zu ihrer richtigen Klasse gehören.

Der Report casts unterscheidet zwischen Methoden mit statischer und dynami-scher Bindung. Statisch gebundene Methoden müssen sich selbst um die Prüfungkümmern. casts und checks generieren lokale Strukturzeiger für die eigenen Para-meterobjekte und Anweisungen, um die anderen Objekte zu überprüfen, das heißt,

Page 114: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

104___________________________________________________________________________8 Dynamische Typprüfung — Defensiv programmieren

%casts muß nach den lokalen Variablen verwendet werden, die am Anfang einerMethode mit statischer Bindung definiert werden.

Dynamisch gebundene Methoden werden nur über Selektoren aufgerufen, des-halb kann man die Prüfung so ziemlich den Selektoren überlassen. %casts wirdauch bei dynamisch gebundenen Methoden verwendet, aber die Anweisung gene-riert nur lokale Strukturzeiger für die Parameterobjekte in der eigenen Klasse:

Circle.dc% Circle draw {%casts

printf("circle at %d,%d rad %d\n",x(self), y(self), self -> rad);

}Point.c

void draw (const void * _self) {const struct Point * self = _self;...

Circle.cstatic void Circle_draw (const void * _self) {

const struct Circle * self = cast(Circle, _self);...

Wir müssen vorsichtig sein: Zwar könnte der Selektor prüfen, ob ein Objekt zur ak-tuellen Klasse Point gehört, aber wenn der Selektor eine Unterklassen-Methodewie Circle_draw() aufruft, müssen wir dort untersuchen, ob das Objekt wirklich einCircle ist. Deshalb lassen wir den Selektor nur die Objekte prüfen, die nicht in deraktuellen Klasse sind, und wir lassen die dynamisch gebundene Methode die Objek-te in ihrer eigenen Klasse untersuchen. Der Report casts ruft einfach checks nichtfür Methoden auf, die durch einen Selektor ausgesucht werden.

Jetzt müssen wir die Selektoren modifizieren. Erfreulicherweise werden sie al-le vom Report init generiert, aber es gibt verschiedene Fälle: Selektoren mit voidliefern kein Resultat; Selektoren mit einer variablen Parameterliste müssen einenZeiger auf die Argumentliste an die eigentliche Methode liefern. init ruft den Re-port selectors in etc.rep auf, der die eigentliche Arbeit vom Report selector und ei-nigen anderen erledigen läßt. Hier ist ein typischer Selektor:

int differ (const void * _self, const void * b) {int result;const struct Class * class = classOf(_self);assert(class -> differ);cast(Object, b);result = class -> differ(_self, b);return result;

}

Page 115: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

105___________________________________________________________________________8.4 Codierstandard

Dies wird vom Report selector in etc.rep generiert:*`%header { `n`%result`%classOf`%ifmethod`%checks`%call`%return} `n `n

Die Reports result und return definieren und liefern die Variable result, wenn derResultattyp nicht void ist:

% result // wenn noetig, Variable result definieren`{ifnot `result void`t `result result;`}n% return // wenn noetig, Variable result liefern`{ifnot `result void`t return result;`}n

Der Report ifmethod untersucht, ob die gewünschte Methode existiert:% ifmethod // pruefen, ob Methode existiert`t assert(class -> `method ); `n

Mit dem Report classOf müssen wir ein bißchen vorsichtig sein: Wenn ein Selek-tor eine Methode der Wurzel-Metaklasse Class holt, können wir uns darauf verlas-sen, daß classOf() eine geeignete Klassenbeschreibung liefert, aber für Unterklas-sen müssen wir kontrollieren:

`{if `meta `metaroot`t const struct `meta * class = classOf(_self); `n`} `{else`t const struct `meta * class = ` \

cast( `meta , classOf(_self)); `n`} `n

Der Oberklassen-Selektor funktioniert ähnlich. Hier ist ein typisches Beispiel:int super_differ (const void * _class, const void * _self,

const void * b) {const struct Class * superclass = super(_class);cast(Object, b);assert(superclass -> differ);return superclass -> differ(_self, b);

}

____________________________________________________________________________________________

* Der eigentliche Report ist ein bißchen komplizierter, denn er muß auch mit Methoden mit variabler Pa-rameterliste fertigwerden.

Page 116: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

106___________________________________________________________________________8 Dynamische Typprüfung — Defensiv programmieren

Wenn wir nicht direkt mit Class arbeiten, müssen wir auch hier das Resultat vonsuper() nochmals überprüfen. Hier ist der Report aus etc.rep:

% super-selector // Oberklassen-Selektor`%super-header { `n`{if `meta `metaroot // super() reicht`t const struct `meta * superclass = super(_class); `n`} `{else // Umwandlung noetig`t const struct `meta * superclass = ` \

cast( `meta , super(_class)); `n`} `n`%checks`t assert(superclass -> `method ); `n`t `{ifnot `result void return `} \

superclass -> `method \( `{() `_ `name `}, `{ifnot `,... ` , app `} ); `n

} `n `nAndere Objekte werden mit checks kontrolliert, als ob der Oberklassen-Selektor ei-ne Methode mit statischer Bindung ist.

Dank ooc und seinen Reports haben wir eine defensive Codiertechnik für alleMethoden entwickelt, die wir vielleicht implementieren. Mit der Änderung an allenSelektoren und mit der Konvention, daß wir %casts in allen Methoden verwenden,erreichen wir alle Objekte, die als Parameter übergeben werden: Ihre Zeiger wer-den beim Import zum Aufrufer kontrolliert. Das Resultat einer Methode müssen wirfolglich nicht kontrollieren, denn wer ein Resultat benützt, kann das nur mit Hilfevon cast() tun.

Aus diesem Grund haben wir auch die Konvention, daß wir Klassen bei den Re-sultattypen unserer Methoden angeben. Zum Beispiel in List.d:

Object @ addFirst (_self, const Object @ element);addFirst() wurde im Abschnitt 7.7 gezeigt. Diese Funktion liefert einen Wert mitdem Typ void *, aber ooc generiert:

struct Object * addFirst (void * _self, const void * element) {struct List * self = cast(List, _self);cast(Object, element);...return (void *) element;

}In einem Anwendungsprogramm ist struct Object ein unvollständiger Typ. DerANSI-C Compiler kontrolliert folglich, daß das Resultat eines Aufrufs von addFirst()nur an void * zugewiesen (und hoffentlich später wieder überprüft) wird, oder daßes einer Methode übergeben wird, die void * erwartet und einen derartigen Zeigernach unseren Konventionen mit cast() untersucht. Allgemein, wenn wir Klassensorgfältig in den Resultattypen unserer Methoden verwenden, können wir denANSI-C Compiler nach relativ sinnlosen Zuweisungen Ausschau halten lassen. EineKlasse als Resultattyp ist wesentlich einschränkender als void *.

Page 117: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

107___________________________________________________________________________8.5 Rekursion vermeiden

8.5 Rekursion vermeidenIm Abschnitt 8.3 versuchten wir, cast() folgendermaßen zu implementieren:

% cast {assert(isOf(_self, class));return (void *) _self;

}Leider sorgt das für endlose Rekursion. Um das zu verstehen, verfolgen wir dieAufrufe, wenn ein Objekt in eine Liste eingetragen wird:

void * list = new(List, 1);void * object = new(Object);

addFirst(list, object) {cast(List, list) {

isOf(list, List) {classOf(list) {

cast(Object, list) {ifOf(list, Object) {

classOf(list) {cast() verwendet isOf(), und diese Methode ruft classOf() und vielleicht auchsuper() auf. Beide Methoden befolgen unseren Codierstandard und importieren ih-re Parameter mit %casts. Diese Anweisung ruft cast() auf, um zu kontrollieren, daßdie Argumente zu den Klassen Object und Class gehören. Unsere Implementie-rung von isOf() im Abschnitt 8.3 ruft classOf() auf, bevor sie das dritte Axiomberücksichtigt, daß jedes Objekt wenigstens zur Klasse Object gehört.

Wie streng soll unsere Typprüfung sein? Wenn wir unserem Code trauen, mußcast() nichts tun und kann durch einen trivialen Makro ersetzt werden. Wenn wirunserem Code nicht trauen, müssen Parameter und alle anderen Objektzugriffe mitAufrufen von cast() in allen Funktionen kontrolliert werden. Jeder muß cast()benützen und dann glauben, und natürlich kann cast() keine anderen Funktionen fürseine Prüfungen verwenden.

Was garantiert nun cast(class, object)? Mindestens so viel wie isOf(), nämlichdaß object kein Nullzeiger ist und daß seine Klassenbeschreibung bis zum Argu-ment class zurückverfolgt werden kann. Wenn wir den Code von isOf() überneh-men und defensiv vorgehen, erhalten wir folgenden Algorithmus:

(_self = self) ist ein Objekt(myClass = self -> class) ist ein Objekt

if (class != Object)class ist ein Objektwhile (myClass != class)

assert(myClass != Object);myClass ist eine KlassenbeschreibungmyClass = myClass -> super;

return self;Die kritischen Fragen sind kursiv: Welcher von Null verschiedene Zeiger verweistauf ein Objekt, wie erkennen wir eine Klassenbeschreibung? Wir können beliebige

Page 118: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

108___________________________________________________________________________8 Dynamische Typprüfung — Defensiv programmieren

Zeiger von Objektzeigern zum Beispiel dadurch unterscheiden, daß wir ein Objektmit einer magischen Zahl beginnen lassen, das heißt, daß wir eine Komponente.magic zur Klassenbeschreibung in Object.d hinzufügen:

% Class Object {unsigned long magic; // magische Zahlconst Class @ class; // Klassenbeschreibung

%...

Wenn die magische Zahl in new() und in der statischen(!) Initialisierung von Classund Object eingefügt wird, können wir nach ihr mit folgenden Makros suchen:

#define MAGIC 0x0effaced // magische Zahl fuer Objekte// efface: sich bescheiden oder scheu unauffaellig verhalten#define isObject(p) \

( assert(p), \assert(((struct Object *) p) -> magic == MAGIC), p )

Streng genommen brauchen wir nicht zu kontrollieren, daß myClass ein Objekt ist,aber die beiden zusätzlichen Aufrufe von assert() sind billig. Wenn wir nicht prüfen,daß class ein Objekt ist, könnte es ein Nullzeiger sein, und dann könnten wir einObjekt mit einem Nullzeiger als Klassenbeschreibung an cast() vorbeimogeln.

Der teure Teil ist die Frage, ob myClass eine Klassenbeschreibung ist. Wir soll-ten nicht sehr viele Klassenbeschreibungen besitzen, und wir sollten sie alle ken-nen, also könnten wir eine Tabelle der legalen Zeigerwerte konsultieren. cast() istaber eine der innersten Funktionen in unserem Code, deshalb sollten wir sie so effi-zient wir möglich machen.

Zunächst einmal ist myClass das zweite Element in einer Zeigerkette von ei-nem Objekt zu seiner Klassenbeschreibung, und bei beiden haben wir bereits diemagische Zahl kontrolliert. Wenn wir das Problem ignorieren, daß Klassenbeschrei-bungen vielleicht durch wilde Zeigerwerte zerstört werden, dürfen wir vernünftiger-weise annehmen, daß die .super-Kette in den Klassenbeschreibungen unbeschädigtbleibt, nachdem sie erst einmal von Class_ctor() aufgebaut wurde. Deshalb entfer-nen wir den Test völlig aus der Schleife und erhalten folgende Implementierung voncast():

static void catch (int sig) // Signal-Behandlung{

assert(sig == 0); // kein Zeiger, sollte nicht vorkommen}% cast {

void (* sigsegv)(int) = signal(SIGSEGV, catch);#ifdef SIGBUS

void (* sigbus)(int) = signal(SIGBUS, catch);#endif

const struct Object * self = isObject(_self);const struct Class * myClass = isObject(self -> class);

Page 119: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

109___________________________________________________________________________8.6 Zusammenfassung

if (class != Object){ isObject(class);

while (myClass != class){ assert(myClass != Object); // falsche Klasse

myClass = myClass -> super;}

}#ifdef SIGBUS

signal(SIGBUS, sigbus);#endif

signal(SIGSEGV, sigsegv);return (void *) self;

}Wir fangen Signale ab, um einen numerischen Wert nicht versehentlich als Zeigerzu betrachten. SIGSEGV wird vom ANSI-C Standard definiert und signalisiert einenillegalen Speicherzugriff. SIGBUS (oder _SIGBUS) ist ein zweites solches Signal, dasviele UNIX-Systeme ebenfalls definieren.

8.6 Zusammenfassungvoid * ist ein sehr toleranter Datentyp, den wir verwenden mußten, um polymor-phe Methoden und insbesondere unseren Mechanismus für die Selektion von dyna-misch gebundenen Methoden zu realisieren. Wenn wir polymorphe Funktionen ha-ben, müssen wir zur Laufzeit eine Typprüfung durchführen, das heißt, immer wennein Objekt als Parameter einer Methode auftaucht.

Objekte zeigen auf eindeutige Klassenbeschreibungen, deshalb können wir ihreTypen dadurch kontrollieren, daß wir die Klassenbeschreibungszeiger mit den be-kannten Klassenbeschreibungen in einem Projekt vergleichen. Wir haben dafür dreineue Methoden definiert: isA() untersucht, ob ein Objekt zu einer bestimmtenKlasse gehört, isOf() ist wahr, wenn ein Objekt zu einer Klasse oder einer ihrer Un-terklassen gehört, und cast() bricht das aufrufende Programm ab, wenn ein Objektnicht als Mitglied einer bestimmten Klasse benützt werden kann.

Als Codierstandard verlangen wir, daß cast() immer benutzt wird, wenn manvon einem Objektzeiger zu einem Strukturzeiger übergeht. Insbesondere müssenMethoden mit statischer Bindung cast() für alle ihre Parameterobjekte verwenden,Selektoren prüfen alle Parameterobjekte, die nicht in ihrer eigenen Klasse sind, undMethoden mit dynamischer Bindung kontrollieren alle Parameterobjekte, die angeb-lich zu ihrer eigenen Klasse gehören. Resultate müssen nicht überprüft werden,denn der Aufrufer kann sie nur mit Hilfe von cast() weiterverwenden.

ooc hilft entscheidend, diesen Codierstandard einzuhalten, denn der Präprozes-sor generiert die Selektoren und expandiert die %casts-Anweisung zum Import vonParametern. %casts generiert die nötigen Aufrufe von cast() und sollte unmittelbarnach den Definitionen der lokalen Variablen in einer Methode verwendet werden.

cast() kann die Korrektheit von Daten zwar nicht beweisen, aber wir machen esziemlich schwierig und unwahrscheinlich, daß cast() besiegt wird. Defensive Pro-

Page 120: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

110___________________________________________________________________________8 Dynamische Typprüfung — Defensiv programmieren

grammierung geht davon aus, daß Programmierer wahrscheinlich Fehler machen,und versucht zu begrenzen, wie lang ein Fehler unerkannt bleibt. cast() ist ein Ba-lanceakt zwischen Effizienz für korrekte Programme und frühzeitiger Entdeckungvon Fehlern.

8.7 ÜberlegungenEigentlich können Oberklassen-Selektoren nur aus Methoden heraus aufgerufenwerden. Wir könnten uns dafür entscheiden, ihre Parameter überhaupt nicht zukontrollieren. Ist das wirklich vernünftig?

Wir glauben, daß ein Zeiger auf ein Objekt verweist, wenn das Objekt mit einermagischen Zahl beginnt. Das ist teuer, weil dadurch alle Objekte etwas größer wer-den. Könnten wir verlangen, daß nur Klassenbeschreibungen mit magischen Zahlenbeginnen?

Der fixe Teil einer Klassenbeschreibung, also Name, Oberklasse und Größe derObjekte, kann mit einer Prüfsumme geschützt werden. Man muß sie allerdingssorgfältig wählen, damit sie in Class und Object statisch initialisiert werden kann.

cast() dupliziert den Algorithmus von isOf(). Können wir isOf() so ändern, daßwir doch die naive Implementierung von cast() verwenden können, ohne in eine un-endliche Rekursion zu geraten?

cast() ist unsere wichtigste Funktion, um Fehlermeldungen zu produzieren. AnStelle von assert() könnten die Meldungen den Punkt des Aufrufs von cast() unddie erwartete und die tatsächlich vorgefundene Klasse enthalten.

Page 121: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

111___________________________________________________________________________

9Statische Konstruktion

Selbstorganisation

9.1 InitialisierungKlassenbeschreibungen sind langlebige Objekte. Sie sind konstant und existieren

praktisch so lange, wie eine Anwendung ausgeführt wird. Nach Möglichkeit sollten

derartige Objekte während der Übersetzung initialisiert werden. Wir haben jedoch

im sechsten Kapitel erkannt, daß statisch initialisierte Klassenbeschreibungen

schwer zu pflegen sind: Die Reihenfolge der Strukturkomponenten muß mit allen

Initialisierungen übereinstimmen, und Vererbung würde uns dazu zwingen, dyna-

misch gebundene Methoden außerhalb ihrer Quelldateien bekanntzumachen.

Als Ausgangsbasis initialisieren wir nur die Klassenbeschreibungen Object und

Class bei der Übersetzung als statische Strukturen in der Datei Object.dc. Alle an-

deren Klassenbeschreibungen werden dynamisch generiert, und die Metaklassen-

Konstruktoren, angefangen mit Class_ctor(), sorgen für binäre Vererbung und erset-

zen dynamisch gebundene Methoden.

ooc generiert Initialisierungsfunktionen, um die Details der Aufrufe von new()zur Erzeugung von Klassenbeschreibungen zu verbergen, aber die Tatsache, daß

diese Initialisierungsfunktionen in einer Anwendung explizit aufgerufen werden

müssen, ist eine Quelle von schwer zu findenden Fehlern. Als Beispiel betrachten

wir initPoint() und initCircle() aus Abschnitt 6.10:

void initPoint (void) {if (! PointClass)

PointClass = new(Class, "PointClass",Class, sizeof(struct PointClass),ctor, PointClass_ctor,0);

if (! Point)Point = new(PointClass, "Point",

Object, sizeof(struct Point),ctor, Point_ctor,draw, Point_draw,0);

}Die Funktion ist so gestaltet, daß sie ihre Arbeit nur einmal tut, das heißt, auch

wenn sie mehrfach aufgerufen wird, erzeugt sie trotzdem nur eine Instanz jeder

Klassenbeschreibung.

Page 122: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

112___________________________________________________________________________9 Statische Konstruktion — Selbstorganisation

void initCircle (void) {if (! Circle){ initPoint();

Circle = new(PointClass, "Circle",Point, sizeof(struct Circle),ctor, Circle_ctor,draw, Circle_draw,0);

}}

Beide Funktionen beachten implizit die Klassenhierarchie: initPoint() sorgt dafür,

daß PointClass existiert, bevor diese Klassenbeschreibung benützt wird, um die

Beschreibung Point zu erzeugen; der Aufruf von initPoint() in initCircle() garan-

tiert, daß die Oberklassen-Beschreibung Point und ihre Metaklassen-Beschreibung

PointClass existieren, bevor wir sie benützen, um die Beschreibung Circle zu er-

zeugen. Hier ist keine Gefahr von Rekursion: initCircle() ruft initPoint() auf, denn

Point ist die Oberklasse von Circle, aber initPoint() kann sich nicht auf initCircle()beziehen, da ooc keine Zyklen in der Oberklassen-Relation erlaubt.

Es geht allerdings schrecklich schief, wenn wir je vergessen, eine Klassenbe-

schreibung zu initialisieren, bevor wir sie benützen. In diesem Kapitel betrachten

wir deshalb Mechanismen, die diesen Fehler automatisch verhindern.

9.2 Initialisierungslisten — munchKlassenbeschreibungen sind praktisch statische Objekte. Sie sollten so lange exi-

stieren, wie das Hauptprogramm aktiv ist. Normalerweise erreicht man das da-

durch, daß man derartige Objekte als globale oder static Variablen definiert und bei

der Übersetzung initialisiert.

Unser Problem besteht darin, daß wir Class_ctor() und die anderen Metaklas-

sen-Konstruktoren aufrufen müssen, um die Details der Vererbung zu verbergen,

wenn wir eine Klassenbeschreibung initialisieren. Funktionsaufrufe können aber nur

zur Laufzeit ausgeführt werden.

Das Problem ist bekannt als sogenannte statische Konstruktoraufrufe — Objek-

te mit einer Lebensdauer wie das Hauptprogramm müssen erzeugt werden, sobald

main() ausgeführt wird. Es gibt keinen Unterschied in der Erzeugung von stati-

schen oder dynamischen Objekten. initPoint() und ähnliche Funktionen vereinfa-

chen die Aufrufkonventionen und ermöglichen Aufrufe in beliebiger Reihenfolge,

aber die eigentliche Arbeit wird in jedem Fall von new() und den Konstruktoren erle-

digt.

Auf den ersten Blick sollte die Lösung völlig trivial sein. Wenn wir annehmen,

daß jede Klassenbeschreibung in einem Programm auch wirklich benützt wird, müs-

sen wir am Anfang von main() jede init-Funktion aufrufen. Leider ist das aber kein

Problem, das einfach durch Textverarbeitung der Quellen gelöst werden kann. oockann hier nicht helfen, denn der Präprozessor weiß — absichtlich — nicht, wie die

Klassen zu Programmen zusammengefügt werden. Die Quellen zu untersuchen

hilft nicht, denn Klassen können auch aus Bibliotheken gebunden werden.

Page 123: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

113___________________________________________________________________________9.2 Initialisierungslisten — ‘‘munch’’

Moderne Link-Programme wie GNU ld ermöglichen, daß der Compiler einen Vek-

tor von Adressen zusammenstellt, zu dem jeder Objektmodul Einträge beisteuern

kann, wenn er in ein Programm gebunden wird. In unserem Fall könnten wir die

Adressen aller init-Funktionen in einem derartigen Vektor sammeln und main() so

modifizieren, daß die Funktionen nacheinander aufgerufen werden. Diese Technik

gibt es jedoch nur für Compiler-Bauer, nicht für Compiler-Benutzer.

Trotzdem sollten wir den Hinweis beherzigen. Wir definieren einen Adreßvek-

tor initializers[] und machen in main() folgendes:

void (* initializers [])(void) = {...

0 };int main (){ extern void (* initializers [])(void);

void (** init)(void) = initializers;while (* init)

(** init ++)();...

Jetzt müssen wir nur noch jede Initialisierungsfunktion in unserem Programm als

Element von initializers[] angeben. Wenn es ein Dienstprogramm wie nm gibt,

das die Symboltabelle eines fertig gebundenen Programms ausgeben kann, können

wir den Vektor ungefähr so automatisch generieren:

$ cc -o task object... libooc.a$ nm -p task | munch > initializers.c$ cc -o task object... initializers.c libooc.a

Wir nehmen an, daß die Bibliothek libooc.a einen Modul initializers.o enthält, der

den Vektor initializers[] so wie oben leer mit dem abschließenden Nullzeiger defi-

niert. Das Link-Programm verwendet diesen Bibliotheksmodul nur, wenn der Vek-

tor nicht in einem Modul definiert wird, der auf der Kommandozeile vor libooc.a an-

gegeben ist.

nm gibt die Symboltabelle von task nach der ersten Übersetzung aus. munchist ein kleines Programm, das einen neuen Modul initializers.c generiert, der auf alle

init-Funktionen in task verweist. Bei der zweiten Übersetzung verwendet das

Link-Programm diesen Modul an Stelle des Bibliotheksmoduls aus libooc.a, um den

richtigen Vektor initializers[] für task zu definieren.

An Stelle eines Vektors könnte munch auch eine Funktion generieren, die alle

Initialisierungsfunktionen aufruft. Wir werden aber im zwölften Kapitel sehen, daß

insbesondere eine Liste von Klassen auch noch für andere Zwecke als nur Initialisie-

rung sehr praktisch ist.

Die Ausgabe von nm hängt im allgemeinen von der Variante von UNIX ab, zu der

der C-Compiler gehört. Glücklicherweise verlangt die Option −p bei Berkeley-nm,

daß keine Sortierung stattfindet, und System-V-nm produziert dann ein kompaktes

Ausgabeformat, das fast wie die Ausgabe von Berkeley-nm aussieht. Hier ist

munch für beide, implementiert mit Hilfe von awk :

Page 124: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

114___________________________________________________________________________9 Statische Konstruktion — Selbstorganisation

NF != 3 || $2 != "T" || $1 !˜ /ˆ[0-9a-fA-F]+$/ {next

}$3 ˜ /ˆ_?init[A-Z][A-Za-z]+$/ {

sub(/ˆ_/, "", $3)names[$3] = 1

}END {

for (n in names)printf "extern void %s (void);\n", n

print "\nvoid (* initializers [])(void) = {"for (n in names)

printf "\t%s,\n", nprint "0 };"

}Die erste Bedingung ignoriert sofort alle Symboltabelleneinträge außer solchen wie

00003ea8 T _initPointWir nehmen an, daß ein Name, der mit init und einem Großbuchstaben beginnt,

und in dem nur Buchstaben folgen, sich auf eine Initialisierungsfunktion bezieht.

Wir entfernen wahlweise einen Unterstrich (manche Compiler produzieren ihn, an-

dere nicht) und speichern den Rest als Index in einen Vektor names[]. Wenn alle

Namen gefunden sind, generiert munch Funktionsprototypen und definiert den Vek-

tor initializers[].Der Vektor names[] wird verwendet, da jeder Name zweimal ausgegeben wer-

den muß. Namen werden als Indizes und nicht als Elementwerte gespeichert, um

Duplikate zu vermeiden.* Mit munch können wir sogar den Bibliotheksmodul mit

einem leeren Vektor erzeugen:

$ munch < /dev/null > initializers.cmunch ist in jedem Fall eine Notlösung: Wir müssen jedes Programm zweimal

montieren, wir benötigen ein Dienstprogramm wie nm mit einem vernünftigen Aus-

gabeformat, und, was das Schlimmste ist, wir verlassen uns auf ein Muster, um die

Initialisierungsfunktionen auszuwählen. munch ist jedoch normalerweise leicht zu

portieren, und die Auswahlmuster kann man für eine Vielzahl von statischen Kon-

struktor-Problemen anpassen. Es ist nicht überraschend, daß das AT&T C++ Sy-

stem für manche Plattformen mit einer (komplizierten) Variante von munch imple-

mentiert wurde.

9.3 Funktionen für Objektemunch ist eine einigermaßen portable, wenn auch ineffiziente Lösung für alle mögli-

chen statischen Initialisierungsprobleme. Klassenbeschreibungen zu erzeugen, be-

vor sie benutzt werden, ist ein einfacher Fall, und es zeigt sich, daß dies wesentlich

leichter und vollständig portabel erreicht werden kann.

____________________________________________________________________________________________

* Duplikate sollten ohnehin nicht vorkommen, denn eine globale Funktion kann in einem Programm nicht

zweimal definiert werden, aber wir gehen immer lieber auf Nummer sicher.

Page 125: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

115___________________________________________________________________________9.3 Funktionen für Objekte

Unser Problem besteht darin, daß wir eine Zeigervariable auf ein Objekt verwei-

sen lassen, daß wir aber einen Funktionsaufruf brauchen, um das Objekt zu erzeu-

gen, wenn es noch nicht existiert. Dies führt zu ungefähr folgender Makrodefiniti-

on:

#define Point (Point ? Point : (Point = initPoint()))Der Makro Point untersucht, ob die Klassenbeschreibung Point schon initialisiert

ist. Wenn nicht, ruft der Makro initPoint() auf, um die Klassenbeschreibung zu er-

zeugen. Wenn wir allerdings Point als Makro ohne Parameter definieren, können

wir den Namen nicht mehr gleichzeitig als Struktur-Etikett für Objekte und für die

zugehörige Klassenbeschreibung verwenden. Der folgende Makro ist besser:

#define Class(x) (x ? x : (x = init ## x ()))Jetzt verwenden wir Class(Point) für die Klassenbeschreibung. initPoint() ruft im-

mer noch new() auf, muß aber jetzt die neue Klassenbeschreibung liefern, das

heißt, jede Klassenbeschreibung benötigt jetzt ihre eigene Initialisierungsfunktion:

const void * Point;const void * initPoint (void) {

return new(Class(PointClass),"Point", Class(Object), sizeof(struct Point),ctor, Point_ctor,draw, Point_draw,(void *) 0);

}Dieser Entwurf berücksichtigt noch immer die Reihenfolge der Klassenhierarchie:

Bevor die Klassenbeschreibung PointClass an new() übergeben wird, sorgt die Ma-

kroexpansion von Class(PointClass) dafür, daß die Beschreibung existiert. Das

Beispiel zeigt, daß wir leere Funktionen initObject() und initClass() bereitstellen

müssen.

Wenn jede Initialisierungsfunktion das initialisierte Objekt liefert, können wir oh-

ne Makros arbeiten und einfach die Initialisierungsfunktion aufrufen, wenn wir auf

das Objekt zugreifen wollen — ein statisches Objekt wird durch seine Initialisie-

rungsfunktion repräsentiert:

static const void * _Point;const void * const Point (void) {

return _Point ? _Point :(_Point = new(PointClass(),

"Point", Object(), sizeof(struct Point),ctor, Point_ctor,draw, Point_draw,(void *) 0));

}Wir könnten den eigentlichen Zeiger _Point auch innerhalb der Funktion definieren;

die globale Definition ist nur notwendig, wenn wir munch für System V implemen-

tieren wollen.

Page 126: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

116___________________________________________________________________________9 Statische Konstruktion — Selbstorganisation

Statische Objekte durch Funktionen zu ersetzen, muß nicht weniger effizient

sein als Makros. ANSI-C erlaubt nicht, daß ein Funktionsresultat das Attribut constoder volatile besitzt, das heißt, das fettgedruckte const-Attribut im Beispiel.* GNU-

C erlaubt eine derartige Vereinbarung und verwendet sie zur Optimierung. Wenn

ein Funktionsresultat so mit const vereinbart wird, darf sein Wert nur von den Argu-

menten abhängen, und der Aufruf darf keine Nebeneffekte produzieren. Der Com-

piler versucht dann, die Anzahl der Aufrufe einer solchen Funktion zu minimieren

und das Resultat wiederzuverwenden.

9.4 ImplementierungWenn wir den Namen einer Klassenbeschreibung wie Point durch einen Aufruf der

Initialisierungsfunktion Point() ersetzen, um die Klassenbeschreibungen automa-

tisch zu generieren, müssen wir alle Stellen modifizieren, wo eine Klassenbeschrei-

bung verwendet wird, und wir müssen die ooc-Reports ein bißchen abändern.

Die Namen von Klassenbeschreibungen werden in Aufrufen von new(), cast(),isA(), isOf() und bei den Aufrufen der Oberklassen-Selektoren verwendet. Daß wir

Funktionen an Stelle von Zeigern verwenden, ist eine neue Konvention, das heißt,

wir müssen die Anwendungsprogramme und die Quelldateien ändern. Ein guter

ANSI-C Compiler (oder die Option −pedantic bei GNU-C) kann hier sehr hilfreich sein:

Alle Versuche, einen Funktionsnamen an void * zu übergeben, sollten markiert wer-

den, das heißt, der Compiler sollte alle Punkte in unseren Quellen markieren, wo

wir vergessen haben, eine leere Argumentliste an einen Klassennamen anzuhän-

gen.

Die Reports zu ändern, ist ein bißchen schwieriger. Es hilft, wenn man in den

generierten Dateien nach Klassenbeschreibungen sucht. Die Repräsentierungsdatei

Point.r bleibt unverändert. Die Schnittstellendatei Point.h deklariert die Zeiger auf

die Klassen- und Metaklassen-Beschreibungen. Hier muß

extern const void * Point;extern const void * PointClass;

in

extern const void * const Point (void);extern const void * const PointClass (void);

abgeändert werden, wobei das fettgedruckte const-Attribut nur mit GNU-C verwen-

det werden kann. Es ist besser, wenn der Report portabel bleibt, deshalb ändern

wir die relevanten Zeilen in h.rep wie folgt ab:

extern const void * `%const `class (void); `n `nextern const void * `%const `meta (void); `n `n

Dann fügen wir einen neuen Report zur gemeinsam verwendeten Datei header.rephinzu:

____________________________________________________________________________________________

* Das erste const-Attribut bedeutet, daß das Resultat der Funktion auf einen konstanten Wert zeigt. Nur

das zweite const-Attribut gibt an, daß der Zeigerwert selbst konstant ist.

Page 127: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

117___________________________________________________________________________9.4 Implementierung

% const // GNUC erlaubt const bei Funktionen`{if `GNUC 1 const `}

ooc definiert das Symbol GNUC normalerweise mit dem Wert Null, aber wenn wir

$ ooc -DGNUC=1 ...aufrufen, können wir das Symbol auf der Kommandozeile auf 1 setzen und damit

besseren Code generieren lassen.

Die Quelldatei Point.c enthält sehr viele Änderungen. Alle Aufrufe von cast()wurden geändert. Sie werden hauptsächlich durch die %casts-Anweisung erzeugt

und stammen deshalb aus den Reports casts und checks, die wir im Abschnitt 8.4

kennengelernt haben. Andere Aufrufe von cast() werden in einigen Selektoren und

Oberklassen-Selektoren und in den Metaklassen-Konstruktoren verwendet, aber

diese werden von Reports in etc.rep, c.rep und c-R.rep generiert. Jetzt zahlt sich

aus, daß wir unseren Codierstandard mit ooc implementiert haben — diesen Stan-

dard können wir leicht zentral ändern.

Die wesentliche Änderung besteht natürlich im neuen Stil der Initialisierungs-

funktionen. Erfreulicherweise werden auch diese in c.rep generiert, und wir ent-

wickeln die neuen Versionen, indem wir Point() aus dem vorigen Abschnitt in einen

Report in c.rep verwandeln. Schließlich müssen wir noch triviale Funktionen wie

const void * const Object (void) {return & _Object;

}im Report init in c-R.rep erzeugen, damit auch sie von der Bedingung GNUC profi-

tieren können. Das ist ein bißchen problematisch, denn wie wir im Abschnitt 7.5

festgestellt haben, muß die statische Initialisierung von _Object in Object.dc codiert

werden:

extern const struct Class _Object;extern const struct Class _Class;%initstatic const struct Class _Object = {

{ MAGIC, & _Class },"Object", & _Object, sizeof(struct Object),Object_ctor, Object_dtor, Object_differ, Object_puto

};extern vereinbart Vorwärtsverweise auf die Beschreibungen. %init generiert die

Funktionen, die auf die Beschreibungen, wie oben gezeigt, zugreifen. static sollte

den initialisierten Beschreibungen schließlich interne Bindung geben, das heißt, sie

sollten noch immer in der Quelldatei Object.c versteckt sein. Manche Compiler

werden hier ein bißchen gestreßt.

Als Ausnahme muß _Object der Name der Struktur selbst sein und kein Zeiger,

der darauf verweist, so daß & _Object zur Initialisierung der Struktur verwendet

werden kann. Wenn wir keinen Makro wie Class() einführen, macht das kaum

einen Unterschied, aber es kompliziert munch ein bißchen:

Page 128: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

118___________________________________________________________________________9 Statische Konstruktion — Selbstorganisation

NF != 3 || $1 !˜ /ˆ[0-9a-f]+$/ { next }$2 ˜ /ˆ[bs]$/ { bsd[$3] = 1; next }$2 == "d" { sysv[$3] = 1; next }$2 == "T" { T[$3] = 1; next }END {

for (name in T)if ("_" name in bsd) # fuehrende _ entfernen

names[n ++] = substr(name, 2)else if ("_" name in sysv)

names[n ++] = namefor (i = 0; i < n; ++ i)

printf "extern const void * %s (void);\n", names[i]print "\nconst void * (* classes [])(void) = {"for (i = 0; i < n; ++ i)

printf "\t%s,\n", names[i]print "0 };"

}Ein Klassenname sollte jetzt als globale Funktion und mit einem führenden Unter-

strich als lokaler Datenwert auftauchen. Berkeley-nm markiert initialisierte lokale

Daten mit s und uninitialisierte Daten mit b, System-V-nm benützt in beiden Fällen

d. Wir sammeln einfach alle interessanten Symbole in drei Vektoren auf und fügen

sie bei der END-Aktion zusammen, um den Vektor names[] zu produzieren, den wir

tatsächlich brauchen. Die Architektur hat sogar einen Vorteil: Wir können einen ein-

fachen Shell-Sort [K&R89] einfügen, um die Klassennamen in alphabetischer Rei-

henfolge auszugeben:

for (gap = int(n/2); gap > 0; gap = int(gap/2))for (i = gap; i < n; ++ i)

for (j = i-gap; j >= 0 && \names[j] > names[j+gap]; j -= gap)

{ name = names[j]names[j] = names[j+gap]names[j+gap] = name

}Wenn wir Funktionsaufrufe an Stelle von Klassennamen verwenden, brauchen wir

munch zur Initialisierung nicht. Eine Liste der Klassen in einem Programm kann

aber für andere Zwecke ganz praktisch sein.

9.5 ZusammenfassungStatische Objekte wie Klassenbeschreibungen würde man normalerweise vor der

Übersetzung initialisieren. Wenn wir Konstruktoren aufrufen müssen, verpacken

wir die Aufrufe in Funktionen ohne Parameter und sorgen dafür, daß diese Funktio-

nen früh genug und in richtiger Reihenfolge aufgerufen werden. Um triviale, aber

sehr schwer zu findende Fehler zu vermeiden, sollten wir einen Mechanismus ent-

wickeln, der diese Funktionsaufrufe automatisch ausführt — unsere Programme

sollten selbst-organisierend sein.

Page 129: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

119___________________________________________________________________________9.6 Überlegungen

Eine Lösung ist eine Montagetechnik, zum Beispiel mit Hilfe eines Programms

wie munch, um einen Vektor mit allen Initialisierungsfunktionen anzulegen. Jedes

Vektorelement wird dann am Anfang des Hauptprogramms aufgerufen. Eine Funkti-

on main() mit einer Schleife, die die Funktionen im Vektor ausführt, kann Teil unse-

rer Projektbibliothek sein und jede Anwendung beginnt einfach mit einer Funktion

mainprog(), die von main() aufgerufen wird.

Eine andere Lösung besteht darin, daß eine Initialisierungsfunktion das initiali-

sierte Objekt als Resultat liefert. Wenn die Funktion so codiert ist, daß sie ihre ei-

gentliche Arbeit nur einmal leistet, können wir jeden Verweis auf ein statisches Ob-

jekt durch einen Aufruf seiner Initialisierungsfunktion ersetzen. Wir können auch

Makros verwenden, um diesen Effekt effizienter zu erzielen. In jedem Fall können

wir dann aber die Adresse eines Verweises auf ein statisches Objekt nicht mehr be-

stimmen, aber da der Verweis selbst ein Zeiger ist, sollte das ohnehin kaum nötig

sein.

9.6 ÜberlegungenDer Makro Class() ist eine effizientere und portable Lösung zur automatischen In-

itialisierung von Klassenbeschreibungen. Zur Implementierung müssen wir wieder

Reports, Klassendefinitionen und Anwendungsprogramme so ändern, wie das in

diesem Kapitel für Initialisierungsfunktionen gezeigt wurde.

munch muß vielleicht für ein neues System portiert werden. Wenn dieses Pro-

gramm zusammen mit dem Makro Class() eingesetzt wird, können wir zum Schluß

die Bedingung aus dem Makro entfernen und alle Klassenbeschreibungen mit Hilfe

von munch initialisieren. Wie initialisieren wir in der richtigen Reihenfolge? Könnte

ooc hier vielleicht helfen (im Manual im Anhang C wird eine Option −M für occ er-

klärt, die fast das Gewünschte leistet)? Was machen wir mit cast() bei einem end-

gültig fertigen System?

Alle Klassenbeschreibungen sollten zuerst in Aufrufen von cast() auftauchen.

Wir können eine fiktive Klasse einführen

typedef const void * (* initializer) (void);% Class ClassInit: Object {

initializer init;%}

und statisch initialisierte Instanzen dieser Klasse als ‘‘uninitialisierte Klassenbe-

schreibungen’’ verwenden:

static struct ClassInit _Point = {{ MAGIC, 0 }, /* Object ohne Klassenbeschreibung */initPoint /* Initialisierungsfunktion */

};const void * Point = & _Point;

cast() kann dann eine Klassenbeschreibung mit einem Nullzeiger an Stelle ihrer ei-

genen Klassenbeschreibung entdecken, annehmen, daß es sich um eine structClassInit handelt, und die Initialisierungsfunktion aufrufen. Diese Lösung reduziert

Page 130: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

120___________________________________________________________________________9 Statische Konstruktion — Selbstorganisation

zwar die Anzahl unnötiger Funktionsaufrufe, aber wie beeinflußt sie die Verwendung

von cast()?

Page 131: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

121___________________________________________________________________________

10Delegates

Callback-Funktionen

10.1 CallbacksEin Objekt zeigt auf seine Klassenbeschreibung. Die Klassenbeschreibung zeigt aufalle dynamisch gebundenen Methoden, die auf das Objekt angewendet werdenkönnen. Es sieht so aus, als ob wir ein Objekt fragen können, ob es eine bestimm-te Methode besitzt. Eigentlich ist das eine Sicherheitsmaßnahme: Bei einem zwei-felhaften Objekt können wir zur Laufzeit untersuchen, ob wir ihm eine bestimmteNachricht schicken können. Wenn wir selbst nicht aufpassen, prüft der Selektor be-stimmt und bricht unser Programm ab, wenn die Methode in der Klassenbeschrei-bung des Objekts nicht vorkommt.

Warum wollen wir das wirklich wissen? Wir haben ohnehin Probleme, wenn ei-ne Methode unbedingt auf ein Objekt angewendet werden soll, für das sie nicht be-stimmt ist — in diesem Fall hilft auch Nachschauen nichts. Wenn es jedoch für un-seren Algorithmus keine Rolle spielt, ob eine Methode aufgerufen wird oder nicht,dann können wir eine freundlichere Schnittstelle entwickeln, wenn wir nachschauenkönnen.

Die Situation entsteht im Zusammenhang mit Callback-Funkionen. Wenn wirbeispielsweise ein Fenster auf einem Bildschirm managen, möchten manche Be-wohner des Fensters vielleicht informiert werden, wenn sie gerade verdeckt, wie-der sichtbar, vergrößert, verkleinert oder zerstört werden. Wir können unseren Kli-enten informieren, indem wir eine Funktion aufrufen, auf die wir uns beide geeinigthaben: Entweder gab uns der Klient einen Funktionsnamen für ein bestimmtes Er-eignis an, oder wir verwenden einen vorher festgelegten Funktionsnamen.

Die erste Technik, eine Callback-Funktion registrieren, scheint flexibler. Ein Kli-ent registriert Funktionen nur für die Ereignisse, die aus seiner Sicht wichtig sind.Verschiedene Klienten können verschiedene Callback-Funktionen verwenden, undwir benötigen keinen gemeinsamen Namensraum. Sogar ANSI-C verwendet einigeCallback-Funktionen: bsearch() und qsort() erhalten die Vergleichsfunktion, die dieReihenfolge für Suchen und Sortieren festlegt, und atexit() registriert Funktionen,die unmittelbar vor dem Programmende aufgerufen werden sollen.

Wenn wir uns auf spezielle Funktionsnamen einigen, geht es noch leichter: Einmit lex generierter Scanner ruft eine Funktion yywrap() am Ende jeder Eingabedateiauf und setzt seine Arbeit fort, wenn diese Funktion Null liefert. Es ist natürlich un-praktisch, wenn wir mehr als eine solche Funktion in einem Programm brauchen —wenn die Funktion bsearch() annehmen würde, daß ihre Vergleichsfunktion immercmp heißt, wäre sie wesentlich weniger flexibel zu verwenden.

Page 132: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

122___________________________________________________________________________10 Delegates — Callback-Funktionen

10.2 Abstrakte BasisklassenWenn wir dynamisch gebundene Methoden betrachten, schränken festgelegte Me-thodennamen für Callback-Zwecke offenbar nicht besonders ein. Eine Methodewird für ein bestimmtes Objekt aufgerufen, das heißt, welcher Code für einen Call-back ausgeführt wird, hängt noch von einem Objekt zusätzlich zu dem dafür festge-legten Methodennamen ab.

Methoden können aber nur für eine Klasse vereinbart werden. Wenn wir mit ei-nem Klienten im Stil von Callback-Funktionen kommunizieren wollen, müssen wir ei-ne abstrakte Basisklasse mit den nötigen Kommunikationsmethoden erfinden, unddas Klientenobjekt muß zu einer Unterklasse gehören, in der diese Methoden imple-mentiert sind. Zum Beispiel:

% OrderedClass: Class OrderedArray: Object {%-

int cmp (const _self, int a, int b);void swap (_self, int a, int b);

%}Ein Sortieralgorithmus kann cmp() verwenden, um zwei Vektorelemente auf der Ba-sis von Indizes zu vergleichen, und mit swap() kann er sie umgekehrt anordnen las-sen, wenn die Reihenfolge nicht stimmt. Der Sortieralgorithmus kann auf jede Un-terklasse von OrderedArray angewendet werden, die diese Methoden implemen-tiert. OrderedArray ist eine abstrakte Basisklasse, denn in dieser Klasse selbstwerden die beiden Methoden nur deklariert, nicht implementiert. Von einer abstrak-ten Basisklasse sollte man keine Objekte erzeugen, wenn manche Methoden nichtdefiniert sind.

Abstrakte Basisklassen sind sehr elegant, um Aufrufkonventionen zu verkap-seln. In einem Betriebssystem könnte es zum Beispiel eine abstrakte Basisklassefür eine Art von Gerätetreibern geben. Das Betriebssystem verhandelt dann mit ei-nem Treiber mit den Methoden der Basisklasse, und jeder Treiber muß alle Metho-den implementieren, um sein Gerät zu kontrollieren.

Leider müssen aber immer alle Methoden einer abstrakten Basisklasse für denKlienten implementiert werden, denn die Methoden werden wirklich aufgerufen.Bei einem Gerätetreiber ist das wahrscheinlich üblich, aber ein Gerätetreiber istauch weniger repräsentativ für Callback-Funktionen. Ein Window ist wesentlich ty-pischer: Manche Klienten müssen sich um Sichtbar-Werden kümmern, andern istdas völlig egal — warum sollten sie alle alle Methoden implementieren?

Eine abstrakte Basisklasse schränkt auch die Architektur einer Klassenhierar-chie ein. Ohne mehrfache Vererbung muß ein Klient zu einem bestimmten Teil-baum der Klassenhierarchie gehören, der die abstrakte Basisklasse als Wurzel hat,unabhängig davon, welche Rolle der Klient in einer Anwendung spielt. Als Beispielbetrachten wir einen Klienten, der in einem Window eine Liste grafischer Objektekontrolliert. Die elegante Lösung besteht darin, den Klienten zu einer Unterklassevon List gehören zu lassen, aber die Implementierung von Windows zwingt den Kli-enten, so etwas wie ein WindowHandler zu sein. Wie wir im Abschnitt 4.9 be-sprochen haben, können wir ein Aggregat verwenden und den Klienten ein List-

Page 133: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

123___________________________________________________________________________10.3 Delegates

Objekt enthalten lassen, aber damit beugt sich unsere Klassenhierarchie dem Diktatdes Systems und entwickelt sich nicht nach den Notwendigkeiten unserer Anwen-dungen.

Schließlich hat eine abstrakte Basisklasse, die Callback-Funktionen deklariert,oft keine privaten Datenkomponenten für ihre Objekte, das heißt, die Klasse dekla-riert zwar Methoden, definiert sie aber nicht, und ihre Objekte haben keinen priva-ten Zustand. Das Klassenkonzept verbietet so etwas zwar nicht, aber es ist dannwirklich keine typische Klasse. Die Vermutung drängt sich auf, daß eine abstrakteBasisklasse eher eine Sammlung von Funktionen ist als eine Sammlung von Objek-ten und Methoden.

10.3 DelegatesNachdem wir nun abstrakte Basisklassen mehr oder weniger verdammt haben,müssen wir nach einer besseren Idee suchen. Für einen Callback braucht man zweiPartner: Das Klientenobjekt möchte aufgerufen werden, und der Gastgeber ruftauf. Offensichtlich muß sich das Klientenobjekt beim Gastgeber anmelden, wennes von ihm eine Nachricht bekommen will, aber das ist auch schon alles, wenn derGastgeber den Klienten fragen kann, welche Callbacks er empfangen möchte, dasheißt, welche Methodenaufrufe erlaubt sind.

Unsere Perspektive hat sich damit entscheidend verlagert: Jetzt gehört ein Ob-jekt zum Callback-Szenario. Wir nennen das Klientenobjekt einen Delegate. Wennsich der Delegate beim Gastgeber anmeldet, untersucht der Gastgeber, welcheCallbacks der Delegate verarbeiten kann; später schickt der Gastgeber dann genaudie Nachrichten, die der Delegate erwartet.

Als Beispiel implementieren wir ein einfaches Rahmenprogramm für Text-Filter,das heißt, ein Programm, das Zeilen aus der Standard-Eingabe oder aus Dateienliest, die als Argumente angegeben sind, das die Zeilen bearbeitet und die Resultateals Standard-Ausgabe liefert. Als eine Anwendung betrachten wir ein Programm,das Zeilen und Zeichen in einer Textdatei zählt. Hier ist das Hauptprogramm, dasals Teil der Implementierungsdatei Wc.dc formuliert werden kann:

int main (int argc, char * argv []){ void * filter = new(Filter(), new(Wc()));

return mainLoop(filter, argv);}

Wir erzeugen ein allgemeines Filter-Objekt filter und geben ihm als Delegate einanwendungsspezifisches Wc-Objekt, das Zeilen und Zeichen zählt. filter erhält dieArgumente unseres Programms und betreibt die Hauptschleife mainLoop(), ausder Callbacks zum Wc-Objekt erfolgen.

% WcClass: Class Wc: Object {unsigned lines; // Zeilen in aktueller Dateiunsigned allLines; // Zeilen in vorherigen Dateienunsigned chars; // Bytes in aktueller Dateiunsigned allChars; // Bytes in vorherigen Dateienunsigned files; // erledigte Dateien

Page 134: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

124___________________________________________________________________________10 Delegates — Callback-Funktionen

%-int wc (_self, const Object @ filter, \

const char * fnm, char * buf);int printFile (_self, const Object @ filter, \

const char * fnm);int printTotal (_self, const Object @ filter);

%}Die Methoden der Klasse Wc tun nichts, als Zeilen und Zeichen zu zählen und dieResultate zu berichten. wc() wird mit einem Puffer aufgerufen, der eine Zeileenthält:

% Wc wc { // (self, filter, fnm, buf)%casts

++ self -> lines;self -> chars += strlen(buf);return 0;

}Nachdem eine Datei bearbeitet wurde, gibt printFile() die Statistik aus und addiertsie zur laufenden Summe hinzu:

% Wc printFile { // (self, filter, fnm)%casts

if (fnm && strcmp(fnm, "-"))printf("%7u %7u %s\n",

self -> lines, self -> chars, fnm);else

printf("%7u %7u\n", self -> lines, self -> chars);self -> allLines += self -> lines, self -> lines = 0;self -> allChars += self -> chars, self -> chars = 0;++ self -> files;return 0;

}fnm ist ein Argument mit dem aktuellen Dateinamen. Dabei kann ein Nullzeigeroder ein Minuszeichen angegeben werden; in diesem Fall geben wir keinen Datei-namen aus.

Zum Schluß gibt printTotal() die Gesamtsumme aus, wenn printFile() mehr alseinmal aufgerufen wurde:

% Wc printTotal { // (self, filter)%casts

if (self -> files > 1)printf("%7u %7u in %u files\n",

self -> allLines, self -> allChars, self -> files);return 0;

}Wc beschäftigt sich nur mit Zählen und kümmert sich nicht um Kommandozei-

lenargumente, Zugriff auf Dateien, Eingabe etc. Dateinamen werden nur in derAusgabe gezeigt, sie haben sonst keine Bedeutung.

Page 135: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

125___________________________________________________________________________10.4 Ein Rahmenprogramm — ‘‘Filter’’

10.4 Ein Rahmenprogramm — FilterEine Kommandozeile zu verarbeiten, ist ein allgemeines Problem, das alle Filterpro-gramme lösen müssen. Wir müssen einzelne oder gebündelte Flaggen und Opti-onswerte einsammeln, wir müssen ein Argument aus zwei Minuszeichen als Endeder Kommando-Optionen erkennen und ein einzelnes Minuszeichen zusätzlich alsStandard-Eingabe, und wir müssen vielleicht die Standard-Eingabe oder die angege-benen Dateien lesen. Jedes Filterprogramm enthält mehr oder weniger den glei-chen Code für diesen Zweck, und Makros wie MAIN [Sch87, Kapitel 15] oder Funk-tionen wie getopt(3) helfen zwar, Standards einzuhalten, aber warum schreiben wirüberhaupt immer wieder den gleichen Code?

Die Klasse Filter ist als einheitliche Implementierung der Bearbeitung von Kom-mandozeilen für alle Filterprogramme entworfen. Wir können dies ein Rahmenpro-gramm (application framework ) nennen, denn diese Klasse legt die grundsätzlichenRegeln und die Struktur für eine große Gruppe von Anwendungen fest. Die Metho-de mainLoop() verarbeitet ein für alle Male die Kommandozeile und verwendet Call-back-Funktionen, um dem Klienten die extrahierten Argumente zu übergeben:

% mainLoop { // (self, argv)%casts

self -> progname = * argv ++;while (* argv && ** argv == ’-’){ switch (* ++ * argv) {

case 0: // ein einziges --- * argv; // ... ist Dateinamebreak; // ... und beendet Optionen

case ’-’:if (! (* argv)[1]) // zwei --{ ++ argv; // ... werden ignoriert

break; // ... und beendet Optionen}

default: // Rest sind Flaggenbuendeldo

if (self -> flag){ self -> argv = argv;

self -> flag(self -> delegate,self, ** argv);

argv = self -> argv;}else{ fprintf(stderr,

"%s: -%c: no flags allowed\n",self -> progname, ** argv);

return 1;}

while (* ++ * argv);++ argv;continue;

}break;

}

Page 136: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

126___________________________________________________________________________10 Delegates — Callback-Funktionen

Die äußere Schleife verarbeitet Argumente, bis wir den Nullzeiger erreichen, derden Vektor argv[] beendet, oder bis ein Argument nicht mehr mit einem Minuszei-chen beginnt. Ein oder zwei Minuszeichen beenden die äußere Schleife mit break-Anweisungen.

Die innere Schleife liefert jedes Zeichen eines Arguments an die flag-Methode,die der Delegate besitzen kann. Wenn der Delegate beschließt, daß zu einer Flaggeein Optionswert gehört, dient die Methode argval() als Callback vom Delegatezurück zum Filter, um den Optionswert abzuholen:

% argval { // (self)const char * result;

%castsassert(self -> argv && * self -> argv);if ((* self -> argv)[1]) // -fvalue

result = ++ * self -> argv;else if (self -> argv[1]) // -f value

result = * ++ self -> argv;else // kein Argument mehr

result = NULL;while ((* self -> argv)[1]) // an Text vorbei

++ * self -> argv;return result;

}Der Optionswert ist entweder der Rest des Flaggen-Arguments oder das nächsteArgument, falls es noch eines gibt. self −> argv wird so weitergerückt, daß die in-nere Schleife von mainLoop() terminiert.

Wenn die Optionen aus der Kommandozeile verarbeitet sind, bleiben die Datei-namenargumente übrig. Gibt es keine, bearbeitet ein Filterprogramm normalerwei-se seine Standard-Eingabe. mainLoop() fährt folgendermaßen fort:

if (* argv)do

result = doit(self, * argv);while (! result && * ++ argv);

elseresult = doit(self, NULL);

if (self -> quit)result = self -> quit(self -> delegate, self);

return result;}

Wir lassen eine Methode doit() einen einzelnen Dateinamen bearbeiten. Ein Null-zeiger bedeutet, daß es keine Argumente gibt. doit() liefert ein Resultat: Nurwenn es Null ist, bearbeiten wir weitere Argumente.

% doit { // (self, arg)FILE * fp;int result = 0;

%casts

Page 137: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

127___________________________________________________________________________10.4 Ein Rahmenprogramm — ‘‘Filter’’

if (self -> name)return self -> name(self -> delegate, self, arg);

if (! arg || strcmp(arg, "-") == 0)fp = stdin, clearerr(fp);

else if (! * arg){ fprintf(stderr, "%s: null filename\n",

self -> progname);return 1;

}else if (! (fp = fopen(arg, "r"))){ perror(arg);

return 1;}

Der Klient kann eine Methode besitzen, um einen Dateinamen selbst zu verarbei-ten. Andernfalls greift doit() auf stdin für einen Nullzeiger oder ein Minuszeichenals Argument zu; auf andere Dateinamen wird Lesezugriff eingerichtet. Wenn einDateizugriff existiert, kann der Klient mit einer weiteren Callback-Funktion eingrei-fen, oder doit() legt einen dynamischen Puffer an und beginnt, Zeilen zu lesen:

if (self -> file)result = self -> file(self -> delegate, self, arg, fp);

else{ if (! self -> buf)

{ self -> blen = BUFSIZ;self -> buf = malloc(self -> blen);assert(self -> buf);

}while (fgets(self -> buf, self -> blen, fp))

if (self -> line && (result =self -> line(self -> delegate, self, arg,

self -> buf)))break;

if (self -> wrap)result = self -> wrap(self -> delegate, self, arg);

}if (fp != stdin)

fclose(fp);if (fflush(stdout), ferror(stdout)){ fprintf(stderr, "%s: output error\n", self -> progname);

result = 1;}return result;

}Mit zwei weiteren Callback-Funktionen kann der Klient jede Textzeile empfangenund am Schluß einer Datei aufräumen. Diese Funktionen wurden in unserem Bei-spiel wc verwendet. doit() gibt den FILE-Zeiger frei und kontrolliert, daß die Ausga-be erfolgreich geschrieben wurde.

Page 138: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

128___________________________________________________________________________10 Delegates — Callback-Funktionen

Wenn eine Klientenklasse zeilenorientierte Callbacks für die Filter-Klasse imple-mentiert, muß sie sich darüber im Klaren sein, daß sie Textzeilen verarbeitet.fgets() liest, bis der Puffer voll ist, oder bis ein Zeilentrenner gefunden wird. Zu-sätzlicher Code in doit() vergrößert den Puffer bei Bedarf dynamisch, aber der Kli-ent erhält nur den Puffer, keine Pufferlänge. fgets() liefert die Anzahl der eingelese-nen Zeichen nicht, das heißt, wenn die Eingabe ein Null-Byte enthält, kommt der Kli-ent nicht daran vorbei, denn das Null-Byte könnte das Ende des letzten Puffers ei-ner Datei markieren, die keinen abschließenden Zeilentrenner enthält.

10.5 Die Methode respondsToWie erreicht ein Objekt seinen Delegate? Wenn ein Filter-Objekt erzeugt wird, er-hält es das Delegate-Objekt als ein Argument. Die Klassenbeschreibung in Filter.ddefiniert Funktionszeigertypen für die möglichen Callback-Funktionen und Objekt-komponenten, um diese Zeiger aufzubewahren:

typedef void (* flagM) (void *, void *, char);typedef int (* nameM) (void *, const void *, const char *);typedef int (* fileM) (void *, const void *, const char *,

FILE *);typedef int (* lineM) (void *, const void *, const char *,

char *);typedef int (* wrapM) (void *, const void *, const char *);typedef int (* quitM) (void *, const void *);% Class Filter: Object {

Object @ delegate;flagM flag; // Flagge verarbeitennameM name; // Dateiname verarbeitenfileM file; // offene Datei verarbeitenlineM line; // Zeilenpuffer verarbeitenwrapM wrap; // eine Datei fertigquitM quit; // alle Dateien fertigconst char * progname; // argv[0]char ** argv; // aktuelles Argument/Bytechar * buf; // dynamischer Zeilenpufferunsigned blen; // aktuelle max. Laenge

%int mainLoop (_self, char ** argv);const char * argval (_self);const char * progname (const _self);int doit (_self, const char * arg);

%}Leider kann man bei ANSI-C mit typedef keinen Funktionskopf definieren, aber eineKlientenklasse wie Wc kann mit den Funktionszeigertypen immer noch kontrollie-ren, daß eine Callback-Funktion den Erwartungen von Filter entspricht:

Page 139: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

129___________________________________________________________________________10.5 Die Methode ‘‘respondsTo’’

#include "Filter.h"% Wc wc { // (self, filter, fnm, buf)%casts

assert((lineM) wc == wc);...

Die assert-Bedingung ist trivialerweise wahr, aber ein guter ANSI-C Compiler wirdsich über einen Typfehler beschweren, wenn lineM dem Typ von wc() nicht ent-spricht:

In function `Wc_wc’:warning: comparison of distinct pointer types lacks a cast

Wir haben allerdings noch immer nicht gesehen, woher unser filter weiß, daßer wc() aufrufen muß, um eine Eingabezeile zu bearbeiten. Filter_ctor() erhält dasDelegate-Objekt als Argument und kann die interessanten Komponenten in filter in-itialisieren:

% Filter ctor {struct Filter * self = super_ctor(Filter(), _self, app);self -> delegate = va_arg(* app, void *);self -> flag = (flagM) respondsTo(self -> delegate, "flag");...self -> quit = (quitM) respondsTo(self -> delegate, "quit");return self;

}Der Trick besteht in einer neuen statisch gebundenen Methode respondsTo(), dieauf jedes Object angewendet werden kann. Die Methode erhält ein Objekt undeinen Suchschlüssel und liefert einen geeigneten Funktionszeiger, wenn das Objekteine dynamisch gebundene Methode hat, die dem Suchschlüssel entspricht.

Der Funktionszeiger könnte ein Selektor oder die Methode selbst sein. Wennwir die Methode als Resultat liefern, vermeiden wir den Selektor-Aufruf, wenn dieCallback-Funktion aufgerufen wird, aber wir vermeiden auch die Überprüfung derParameter, die der Selektor vornimmt. Wir gehen auf Nummer sicher und lassenrespondsTo() nur den Selektor liefern.

Der Entwurf des Suchschlüssels ist etwas schwieriger. Da respondsTo() eineallgemeine Methode für alle Methodentypen ist, können wir beim Übersetzen keineTypprüfung vornehmen, aber wir haben bei Wc_wc() gesehen, wie sich der Dele-gate zur Laufzeit schützen kann. Unabhängig von der Typprüfung könnten wirrespondsTo() immer noch nach dem Selektor suchen lassen, der als Resultat gelie-fert werden soll, das heißt, der Suchschlüssel könnte der gewünschte Selektorsein. Selektor-Namen sind aber Teil des globalen Namensraums eines Programms,das heißt, wenn wir nach einem Selektor-Namen suchen, können wir implizit nurnoch Unterklassen der Klasse durchsuchen, in der der Selektor eingeführt wurde.Wir wollten aber gerade nicht durch Vererbung eingeschränkt werden. Deshalb ver-wendet respondsTo() eine Zeichenkette als Suchschlüssel.

Page 140: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

130___________________________________________________________________________10 Delegates — Callback-Funktionen

Damit bleibt das Problem, wie wir eine Zeichenkette mit einer dynamisch ge-bundenen Methode verknüpfen. Aus logischer Sicht bieten sich zwei Stellen an:Wenn die Methode in der Klassenbeschreibungsdatei deklariert wird, oder jedes-mal, wenn sie implementiert wird. In jedem Fall ist das eine Aufgabe für ooc, dennder Zusammenhang zwischen der Zeichenkette und dem Methodennamen muß inder Klassenbeschreibung gespeichert werden, damit respondsTo() dort suchenkann; die Klassenbeschreibung wird aber von ooc erzeugt. Wir erweitern unsereSyntax ein bißchen:

% WcClass: Class Wc: Object {...

%-line: int wc (_self, const Object @ filter, \

const char * fnm, char * buf);wrap: int printFile (_self, const Object @ filter, \

const char * fnm);quit: int printTotal (_self, const Object @ filter);%}

In einer Klassenbeschreibung wie Wc.d kann eine Zeichenkette als Markierung voreiner dynamisch gebundenen Methode angegeben werden. Nach Voreinstellungwird der Methodenname selbst verwendet. Eine explizit leere Markierung verhin-dert, daß respondsTo() die Methode findet. Die Markierungen gelten für dyna-misch gebundene Methoden, das heißt, sie werden vererbt. Um das Verfahrennoch flexibler zu machen, kann ein Methodenkopf auch noch bei der Implementie-rung markiert werden. Diese Zeichenkette gilt dann nur für die aktuelle Klasse.

10.6 ImplementierungrespondsTo() muß in der Klassenbeschreibung nach einer Zeichenkette suchen undden zugehörigen Selektor liefern. Bisher enthält die Klassenbeschreibung nur Zei-ger auf die Methoden. Offensichtlich müssen wir den Eintrag für eine Methode inder Klassenbeschreibung erweitern:

typedef void (* Method) (); // fuer respondsTo()%protstruct Method {

const char * tag; // fuer respondsTo()Method selector; // Resultat von respondsTo()Method method; // vom Selektor gewaehlt

};% Class Object {

...Method respondsTo (const _self, const char * tag);

Method ist ein einfacher Funktionszeigertyp, der in der Schnittstellendatei fürObject vereinbart wird. Jede Methode wird in einer Klassenbeschreibung als Kom-ponente mit Typ struct Method eingetragen, also mit Zeigern auf die Zeichenkette,den Selektor und die eigentliche Methode. respondsTo() liefert als Resultat einen

Page 141: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

131___________________________________________________________________________10.6 Implementierung

Zeigerwert vom Typ Method. ANSI-C Compiler erlauben keine impliziten Umwand-lungen mit diesem Typ.

Für diese Architektur müssen wir noch ein paar Änderungen durchführen. InObject.dc müssen wir die statischen Initialisierungen der KlassenbeschreibungenObject und Class ändern und struct Method sowie Method verwenden:

static const struct Class _Object = {{ MAGIC, & _Class },"Object", & _Object, sizeof(struct Object),{ "", (Method) 0, (Method) Object_ctor },{ "", (Method) 0, (Method) Object_dtor },{ "differ", (Method) differ,(Method) Object_differ },...

};Der Report −r in r.rep verwendet den Report link in va.rep , um einen Eintrag in derKlassenbeschreibung für die Repräsentierungsdatei zu erzeugen. Die neue Versiondieses Reports ist sehr einfach:

% link // Komponente der Metaklassenstrukturstruct Method `method ;

Der Report init in c.rep und c-R.rep verwendet meta-ctor-loop in etc.rep, um dieSchleife zu generieren, die die Klassenbeschreibung dynamisch füllt. Auch hiermüssen wir mit den neuen Typen arbeiten:

% meta-ctor-loop // Selektor/Marke/Methode fuer `class`t while ((selector = va_arg(ap, Method))) `n`t { `t const char * tag = va_arg(ap, ` \

const char *); `n`t `t Method method = va_arg(ap, Method); `n `n

`{%- `%link-it `}`t } `n% link-it // Selektor etc. vergleichen und einfuegen`t `t if (selector == (Method) `method ) `n`t `t { `t if (tag) `n`t `t `t `t self -> `method .tag = tag, `n`t `t `t `t self -> `method .selector = selector; `n`t `t `t self -> `method .method = method; `n`t `t `t continue; `n`t `t } `n

An Stelle der Selektor/Methode-Paare übergeben wir jetzt Selektor, Markierung undMethode als Argumente an den Metaklassen-Konstruktor. Dies muß in den Reportinit in c.rep eingebaut werden. Hier ist die Initialisierungsfunktion für Wc, die oocgeneriert:

Page 142: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

132___________________________________________________________________________10 Delegates — Callback-Funktionen

static const void * _Wc;const void * Wc (void) {

return _Wc ? _Wc :(_Wc = new(WcClass(),

"Wc", Object(), sizeof(struct Wc),wc, "line", Wc_wc,printFile, "wrap", Wc_printFile,printTotal, "quit", Wc_printTotal,(void *) 0));

}Nachdem die Klassenbeschreibung jetzt Selektor, Markierung und Methode

enthält, können wir respondsTo() leicht implementieren. Dank der Klassenhierar-chie können wir ausrechnen, wieviele Methoden eine Klassenbeschreibung enthält,und wir können respondsTo() vollständig in der Klasse Object implementieren, ob-gleich die Methode beliebige Klassenbeschreibungen bearbeitet:

% respondsTo {if (tag && * tag) {

const struct Class * class = classOf(_self);const struct Method * p = & class -> ctor; // zuerstint nmeth =

(sizeOf(class) - offsetof(struct Class, ctor))/ sizeof(struct Method); // Anzahl Methoden

doif (p -> tag && strcmp(p -> tag, tag) == 0)

return p -> method ? p -> selector : 0;while (++ p, -- nmeth);

}return 0;

}Der einzige Nachteil ist, daß respondsTo() explizit den allerersten Methodennamenctor enthält, denn davon ausgehend wird die Anzahl der Methoden aus der Größeder Klassenbeschreibung berechnet. Zwar könnte ooc diesen ersten Namen ausder Klassenbeschreibung von Object beschaffen, aber ein Report, mit dem ooc dieMethode respondsTo() allgemeingültig generieren könnte, wäre doch recht auf-wendig.

10.7 Noch eine Anwendung — sortWir wollen ein kleines Textsortierprogramm implementieren, um zu sehen, ob Filterwirklich wiederverwendbar ist, wie Kommandozeilenoptionen benützt werden undum zu erkennen, wie vorteilhaft es ist, daß ein Delegate zu einer beliebigen Klassegehören kann.

Ein Sortierfilter muß alle Textzeilen sammeln, sie anschließend zusammen sor-tieren und schließlich ausgeben. Im Abschnitt 7.7 haben wir eine List-Klasse aufder Basis eines dynamischen Ringpuffers konstruiert, die wir zum Sammeln der Zei-len benutzen können, wenn wir noch eine Sortiermethode hinzufügen. Im Ab-

Page 143: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

133___________________________________________________________________________10.7 Noch eine Anwendung — ‘‘sort’’

schnitt 2.5 haben wir eine einfache String-Klasse implementiert; wenn wir sie mitunserer Klassenhierarchie integrieren, können wir damit jede Zeile in dem List-Objekt speichern.

Beginnen wir mit dem Hauptprogramm, das nur den Filter und seinen Delegateerzeugt.

int main (int argc, char * argv []){ void * filter = new(Filter(), new(Sort(), 0));

return mainLoop(filter, argv);}

Da wir die Callback-Methoden an jede Klasse anfügen dürfen, können wir den Dele-gate direkt als Unterklasse von List erzeugen:

% SortClass: ListClass Sort: List {char rflag;

%-void flags (_self, Object @ filter, char flag);int line (_self, const Object @ filter, const char * fnm, \

char * buf);int quit (_self, const Object @ filter);

%}Als Kommando-Option erkennen wir −r, um rückwärts zu sortieren. Alle anderenFlaggen werden von der Methode flags() abgewehrt, die flag als Markierung fürrespondsTo() verwendet:

% flag: Sort flags {%casts

assert((flagM) flags == flags);if (flag == ’r’)

self -> rflag = 1;else

fprintf(stderr, "usage: %s [-r] [file...]\n",progname(filter)),

exit(1);}

Mit den Klassen String und List sind die Zeilen sehr leicht zu sammeln:% Sort line {%casts

assert((lineM) line == line);addLast(self, new(String(), buf));return 0;

}Nach Voreinstellung erhält die Methode line() die Markierung line.

Wenn alle Zeilen eingelesen sind, kümmert sich die Callback-Methode quit umSortieren und Ausgeben. Wenn überhaupt Zeilen vorhanden sind, lassen wir eineMethode sort() die Liste sortieren; anschließend entfernen wir nacheinander jedeZeile und lassen das String-Objekt sich darstellen. Wir können umgekehrt sortie-ren, wenn wir einfach die Zeilen vom anderen Ende der Liste entfernen:

Page 144: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

134___________________________________________________________________________10 Delegates — Callback-Funktionen

% Sort quit {%casts

assert((quitM) quit == quit);if (count(self)){ sort(self);

doputo(self -> rflag ? takeLast(self)

: takeFirst(self), stdout);while (count(self));

}return 0;

}Wie funktioniert die Methode sort()? ANSI-C definiert die Bibliotheksfunktionqsort(), um beliebige Vektoren in Abhängigkeit von einer Vergleichsfunktion zu sor-tieren. Erfreulicherweise ist List als Ringpuffer in einem Vektor implementiert, dasheißt, wenn wir sort() als Methode von List implementieren, geht das mit sehr ge-ringem Aufwand:

static int cmp (const void * a, const void * b){

return differ(* (void **) a, * (void **) b);}% List sort {%casts

if (self -> count){ while (self -> begin + self -> count > self -> dim)

addFirst(self, takeLast(self));qsort(self -> buf + self -> begin, self -> count,

sizeof self -> buf[0], cmp);}

}Wenn es Listenelemente gibt, rotieren wir die Liste, bis sie in Puffer zusammen-hängt, und übergeben sie dann an qsort(). Die Vergleichsfunktion schickt differ() andie Listenelemente — String_differ() wurde mit strcmp() implementiert und kanndeshalb als Vergleichsfunktion mißbraucht werden.

10.8 ZusammenfassungEin Objekt zeigt auf seine Klassenbeschreibung, und die Klassenbeschreibung zeigtauf alle dynamisch gebundenen Methoden für das Objekt. Deshalb kann man einObjekt fragen, ob es eine bestimmte Methode besitzt. respondsTo() ist eine sta-tisch gebundene Methode für Object. Sie erhält ein Objekt und eine Zeichenketteals Suchschlüssel und liefert den zugehörigen Selektor, wenn der Suchschlüssel zueiner Methode für das Objekt paßt.

Bei ooc können Zeichenketten als Markierungen bei den Prototypen der dyna-misch gebundenen Methoden in der Klassenbeschreibungsdatei angegeben werdensowie als Markierungen an einem Methodenkopf in der Implementierungsdatei; da-

Page 145: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

135___________________________________________________________________________10.9 Überlegungen

bei haben letztere Vorrang. Als Voreinstellung wird der Methodenname als Markie-rung verwendet. Leere Markierungen können nicht gefunden werden. Zur Imple-mentierung von respondsTo() wird ein Selektor zusammen mit der Markierung undder eigentlichen Methode an einen Metaklassen-Konstruktor übergeben. Die Klas-senbeschreibung enthält deshalb für jede Methode auch ihren Selektor und die Mar-kierung.

Mit respondsTo() können wir Delegates implementieren: Ein Klientenobjektmeldet sich als Delegate bei einem Gastgeberobjekt an. Der Gastgeber fragt denKlienten mit respondsTo(), ob er bestimmte Methodenaufrufe beantworten kann.Falls ja, wird der Gastgeber diese Methoden verwenden, um den Klienten über be-stimmte Zustandsänderungen zu informieren.

Wir bevorzugen Delegates gegenüber der Registrierung von Callback-Funktio-nen und abstrakten Basisklassen, um die Kommunikation zwischen einem Gastge-ber und einem Klienten zu definieren. Eine Callback-Funktion kann keine Methodesein, da der Gastgeber kein Objekt hat, für das er die Methode aufrufen kann. Eineabstrakte Basisklasse schränkt die anwendungsorientierte Entwicklung der Klassen-hierarchie unnötig ein. Ähnlich wie bei Callback-Funktionen können wir auch für De-legates genau die Methoden implementieren, die für ein bestimmtes Problem inter-essant sind. Die Menge der möglichen Methoden kann wesentlich größer sein alsdie Menge der Methoden, die ein einzelner Klient verwendet.

Ein Rahmenprogramm besteht aus einem oder mehreren Objekten, die die typi-sche Struktur einer Applikation realisieren. Wenn das Rahmenprogramm gut ent-worfen ist, kann es sehr viel Routine-Codierung einsparen. Delegates sind ein sehrpraktisches Verfahren, um ein Rahmenprogramm mit dem problemspezifischenCode zu verbinden.

10.9 ÜberlegungenFilter implementiert die übliche Kommandozeile, bei der Optionen den Dateinamenvorausgehen, bei der Flaggen zusammengefaßt und Optionswerte zusammen mitFlaggen oder als unabhängige Argumente angegeben werden können. Leider istschon pr(1) ein allgemein verfügbares Programm, das sich nicht an dieses Musterhält. Gibt es eine allgemeine Lösung? Kann eine Flagge vor zwei oder mehr Opti-onswerten stehen, die alle als separate Argumente angegeben sind?

Die Callback-Methode line sollte so abgeändert werden, daß binäre Dateienkorrekt verarbeitet werden können. Ist eine Callback-Methode byte nützlich? Waswäre eine Alternative?

Eine wesentlich effizientere, wenn auch vielleicht nicht portable Implementie-rung würde versuchen, eine Datei im Adreßraum des Programms abzubilden. DieCallback-Methoden müssen dafür nicht unbedingt modifiziert werden, aber mankönnte sie robuster machen.

respondsTo() muß den Namen der ersten Komponente vom Typ structMethod in jeder Klassenbeschreibung kennen. Die Reports −r in r-R.rep oder auchinit in c-R.rep können modifiziert werden, damit man mit einer Struktur dieses Pro-blem umgeht.

Page 146: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

136___________________________________________________________________________10 Delegates — Callback-Funktionen

Der Report init kann modifiziert werden, um eine Methode puto() für Class zugenerieren, die mit der gleichen Technik wie respondsTo() für eine beliebige Klas-senbeschreibung alle Methodenadressen und Markierungen ausgibt.

Wenn wir die Ausgabe unseres sort-Programms zur Kontrolle an das offizielleDienstprogramm sort(1) leiten, erleben wir vielleicht eine Überraschung:

$ sort -r Sort.d | /usr/bin/sort -c -rsort: disorder: int quit (_self, const Object @ filter);

Es gibt effizientere Techniken, mit denen List_sort() die Liste im Ringpufferkompaktieren kann, bevor sie sie an qsort() übergibt. Dürfen wir diese Liste wirk-lich rotieren?

Page 147: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

137___________________________________________________________________________

11Klassenmethoden

Lecks in der Speicherverwaltung

Moderne Arbeitsplatzrechner haben sehr viel Hauptspeicher. Wenn ein Programm

hier und dort ein Byte verliert, macht das wahrscheinlich nicht viel aus. Lecks in der

Speicherverwaltung sind aber normalerweise Anzeichen für Fehler in den Algorith-

men — entweder reagiert das Programm falsch auf unerwartete Eingaben, oder,

noch schlimmer, das Programm ist versehentlich so entworfen, daß es Verbindun-

gen zu dynamisch beschafftem Speicher abbricht. In diesem Kapitel betrachten wir

noch eine allgemeine Technologie, die in der objekt-orientierten Programmierung

verfügbar ist, und mit der wir unter anderem Lecks in der Speicherverwaltung

bekämpfen können.

11.1 Ein BeispielAlle Ressourcen, die sich ein Programm beschafft hat, sollten korrekt freigegeben

werden. Dynamischer Speicher ist eine Ressource, und man sollte Produktionssoft-

ware definitiv auf Lecks in der Speicherverwaltung überprüfen. Als Beispiel be-

trachten wir, was bei einem Syntaxfehler in der Dialogsprache passiert, die wir im

dritten und fünften Kapitel implementiert haben.

$ value(3 * 4) - -bad factor: ’’ 0x0

Der Erkenner versucht, einen Baum für einen Ausdruck zu erzeugen. Wenn etwas

schiefgeht, benützt error() die Funktion longjmp(), um alle Rekursionsebenen zu

verlassen und die Ausführung im Hauptprogramm fortzusetzen. In den Rekursions-

ebenen befinden sich auf dem Stack des C-Programms aber die Teile des Aus-

drucksbaums, die bisher erzeugt wurden. Bei einem Syntaxfehler gehen diese

Stücke verloren: Wir haben ein Leck in der Speicherverwaltung. Dies ist natürlich

ein bekanntes Problem beim Bau von Interpretern.

NeXTSTEP enthält eine einfache Applikation MallocDebug, mit der man wenig-

stens die ernsteren Probleme erkennen kann. Wenn wir den Interpreter value für

unsere Dialogsprache mit der Bibliothek −lMallocDebug binden, werden die Stan-

dardversionen von malloc() und den verwandten Funktionen durch einen Modul er-

setzt, der mit MallocDebug kommunizieren kann. Wir starten zuerst unseren Inter-

preter value und dann die Applikation MallocDebug, verbinden die beiden Prozesse

und drücken auf einen Knopf Leaks, nachdem wir in value die erste Fehlermeldung

erhalten haben. Leider ist die Ausgabe schlicht

No nodes.

MallocDebug verwendet eine relativ naive Methode, um nach Lecks zu suchen:

Die Wörter des Klientenprozesses werden mit einer Liste der Adressen aller dyna-

misch zugeteilten Speicherbereiche verglichen, um zu sehen, ob der Prozeß noch

Page 148: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

138___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

auf alle Flächen zeigt. Als Lecks werden nur die Speicherbereiche angesehen, auf

die kein Wort im Klientenprozeß mehr zeigt. Für die Eingabe

(3 * 4) - -

hat sum() mit Hilfe von product() den ersten Unterbaum schon erzeugt, bevor die

Funktion factor() das Ende der Eingabezeile erreicht. Wenn dann aber error() den

C-Stack von factor() rückwärts bis main() abschneidet, ist die Adresse der Wurzel

dieses Unterbaums noch immer in der lokalen Variablen result von sum() vorhan-

den, und dieses Wort wird zufällig durch longjmp() nicht überschrieben. Die restli-

chen Knoten sind mit der Wurzel in result verbunden, das heißt, aus der Sicht von

MallocDebug können noch immer alle Knoten erreicht werden. Wenn wir allerdings

einen neuen Ausdruck eingeben, wird der alte C-Stack überschrieben und Malloc-Debug findet schließlich das Leck:

value$ value(3 * 4) - -bad factor: ’’ 0x01 + 3

4

MallocDebugZone: Address: Size: Function:default 0x050ec35c 12 mkBin, new, product, sum,

factor, product, sum, stmt

Wenn value entsprechend übersetzt wurde, können wir einen Debugger in einem

zweiten Fenster starten und das Leck anschauen:

$ gdb valueGDB is free software ...(gdb) attach 746Attaching program `value’, pid 7460x5007be2 in read ()(gdb) print * (struct Bin *) 0x050ec35cReading in symbols for mathlib.c...done.$1 = {

type = 0x8024,left = 0x50ec334,right = 0x50ec348

}(gdb) print process(0x050ec35c)Reading in symbols for value.c...done.$3 = void(gdb)

Der GNU-Debugger kann mit einem laufenden Prozeß verbunden werden. Mit printkönnen wir den Inhalt des Leck-Knotens ansehen, wenn wir die Adresse aus dem

MallocDebug-Fenster kopieren und den richtigen Typ angeben: mkBin() hat ur-

sprünglich malloc() aufgerufen, das heißt, wir müssen struct Bin erhalten haben.

Wie die Ausgabe zeigt, kann print im GNU-Debugger sogar eine Methode wie pro-cess() in value aufrufen und das Resultat zeigen. Die Ausgabe von process() er-

scheint natürlich in dem Fenster, in dem value läuft — das Speicherleck erfreut sich

guter Gesundheit:

Page 149: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

139___________________________________________________________________________11.2 Klassenmethoden

$ value(3 * 4) - -bad factor: ’’ 0x01 + 3

412

11.2 KlassenmethodenWie stopfen wir dieses spezielle Loch? Das Leck existiert, wenn error() zur Haupt-

schleife zurückgefunden hat. Entweder sammeln wir alle bereits erzeugten Teile

des Ausdrucksbaums ein und geben sie frei, bevor longjmp() ausgeführt wird, oder

wir benötigen ein alternatives Verfahren, um alle benutzten Knoten freizugeben.

Die Teile einzusammeln ist ein vergebliches Unterfangen, denn sie sind in ver-

schiedenen Aktivierungen der Funktionen versteckt, die an der Methode des rekur-

siven Abstiegs im Erkenner beteiligt sind. Nur jede Aktivierung selbst weiß, was

freigegeben werden muß, das heißt, an Stelle von longjmp() müßten wir in jeder

einzelnen Funktion mit einem Fehler-Resultat fertigwerden. Das geht ziemlich si-

cher schief, wenn das Programm später erweitert wird.

Wir entwerfen lieber eine allgemeine Methode, um Ressourcen wiederzuge-

winnen, denn das ist wesentlich systematischer. Wenn wir wissen, welche Knoten

im Moment für den Ausdrucksbaum angelegt wurden, können wir sie bei einem

Fehler sehr leicht freigeben und den Speicherplatz wiedergewinnen. Wir benötigen

Versionen von new() und delete(), die eine lineare Liste von belegten Knoten unter-

halten, die eine Funktion reclaim() traversieren kann, um den Speicher freizugeben.

Kurz gesagt, für Knoten im Ausdrucksbaum sollten wir abändern, was die Metho-

den new() und delete() tun.

delete() wird auf Objekte angewendet, das heißt, es ist eine Methode, die dy-

namisch gebunden werden kann, damit sie in einem Unterbaum der Klassenhierar-

chie ersetzt werden kann. new() wird jedoch auf eine Klassenbeschreibung ange-

wendet. Wenn wir new() dynamisch binden wollen, müssen wir die Adresse der

Methode zur Klassenbeschreibung des Klassenbeschreibungsobjekts hinzufügen,

an das wir new() senden wollen:

NodeClass

?

"NodeClass"

?

sizeof Node

Node füllen

return 0

aNode erzeugen

struct NodeMetaClass

ctor:

dtor:

new:

Node

"Node"

Object

sizeof aNode

aNode füllen

aNode leeren

aNode freigeben

aNode bewerten

struct NodeClass

ctor:

dtor:

delete:

exec:

aNode

...

struct Node

Page 150: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

140___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

Mit dieser Anordnung können wir new() dynamische Bindung für den Aufruf

new(Node, ...)

geben. Wir handeln uns allerdings ein Problem für die Beschreibung von Klassenbe-

schreibungen ein, das heißt, am rechten Rand dieser Abbildung. Wenn wir neue

Methodenkomponenten in Metaklassen-Beschreibungen wie NodeClass einführen,

können wir sie nicht länger in struct Class speichern, das heißt, unsere Abbildung

müßte rechts wenigstens um eine Ebene verlängert werden, bevor wir wieder auf

Class zurückkommen.

Warum haben wir Methoden in Klassenbeschreibungen gespeichert? Wir neh-

men an, daß wir viele Objekte und wenige Klassen haben. Wenn wir Methoden in

Klassenbeschreibungen statt in Objekten speichern, kostet das einen Zeigerver-

weis, das heißt, wir müssen im Selektor von einem Objekt zu seiner Klassenbe-

schreibung gehen, bevor wir die Methode aufrufen können, aber wir vermeiden den

hohen Speicherverbrauch, der entsteht, wenn jedes Objekt selbst alle seine Metho-

denzeiger enthält.

Es gibt weniger Klassenbeschreibungen als andere Objekte, deshalb ist es

längst nicht so teuer, wenn wir eine Methodenadresse direkt in der Klassenbe-

schreibung speichern, auf die die Methode angewendet wird. Wir nennen derartige

Methoden Klassenmethoden — sie werden direkt auf die Klassenbeschreibung an-

gewendet, in der sie gespeichert sind, und nicht auf die Objekte, die sich diese

Klassenbeschreibung teilen.

Eine typische Klassenmethode ist new(); sie würde ersetzt werden, wenn man

die Speicherverwaltung beeinflussen will: Um Statistiken oder einen Mechanismus

zur Wiedergewinnung zu einzubauen, um Objekte in Speicherzonen anzulegen und

damit die Paging-Eigenschaften eines Programms zu verbessern, um Objekten ge-

meinsamen Speicher zuzuteilen etc. Andere Klassenmethoden können zum Bei-

spiel dann eingeführt werden, wenn wir die Konvention umgehen wollen, daß

new() immer den Konstruktor ctor() aufruft.

11.3 Klassenmethoden implementierenDer interne Unterschied zwischen einer Klassenmethode und einer anderen dyna-

misch gebundenen Methode ist im Selektor verkapselt. Betrachten wir exec(), eine

dynamisch gebundene Methode, die einen Knoten bewertet. Der Selektor verwen-

det classOf(), um die Klassenbeschreibung zu finden, und sucht dort nach der Kom-

ponente .exec:

double exec (const void * _self) {const struct NodeClass * class =

cast(NodeClass(), classOf(_self));

assert(class -> exec.method);return ((double (*) ()) class -> exec.method)(_self);

}

Betrachten wir im Gegensatz dazu new(), eine Klassenmethode, die auf eine Klas-

senbeschreibung angewendet wird. In diesem Fall bezieht sich self auf die Klas-

senbeschreibung selbst, und der Selektor sucht nach .new als Komponente von

*self:

Page 151: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

141___________________________________________________________________________11.3 Klassenmethoden implementieren

struct Object * new (const void * _self, ...) {struct Object * result;va_list ap;const struct Class * self = cast(Class(), _self);

assert(self -> new.method);va_start(ap, _self);result = ((struct Object * (*) ()) self -> new.method)

(_self, & ap);va_end(ap);return result;

}

Hier ist eine Abbildung, die die Bindung von exec() und new() darstellt:

NodeClass

Class

"NodeClass"

Class

sizeof Node

Node füllen

unmöglich

nichts tun

Node erzeugen

struct Class

ctor:

dtor:

delete:

new:

Node

"Node"

Object

sizeof aNode

aNode füllen

aNode leeren

aNode freigeben

aNode erzeugen

aNode bewerten

struct NodeClass

ctor:

dtor:

delete:

new:

exec:

aNode

...

struct Node

Klassenmethoden und dynamisch gebundene Methoden verwenden den glei-

chen Oberklassen-Selektor, denn er erhält den Zeiger auf die Klassenbeschreibung

als explizites Argument.

struct Object * super_new (const void * _class,const void * _self, va_list * app) {

const struct Class * superclass = super(_class);

assert(superclass -> new.method);return

((struct Object * (*) ()) superclass -> new.method)(_self, app);

}

Selektoren werden von ooc mit dem Report selectors in etc.rep erzeugt. Da

die Selektoren für Klassenmethoden und dynamisch gebundene Methoden ver-

schieden sind, müssen in ooc dynamisch gebundene Methoden und Klassenmetho-

den unterschieden werden. Deshalb werden Klassenmethoden in der Klassenbe-

schreibung nach den dynamisch gebundenen Methoden und dem Trenner %+ ange-

geben. Hier ist ein Ausschnitt aus Object.d:

Page 152: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

142___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

% Class Object {...

%const Class @ classOf (const _self); // Klasse

...%-

void * ctor (_self, va_list * app); // Konstruktor...

void delete (_self); // Instanz freigeben%+

Object @ new (const _self, ...); // Instanz erzeugen%}

delete() wurde unter die dynamisch gebundenen Methoden aufgenommen, und

new() ist jetzt eine Klassenmethode.

% Class Class: Object {...

%Object @ allocate (const _self); // Speicher fuer Instanz...

%}

Wenn wir new() als statisch gebundene Methode aus Class entfernen, verpacken

wir die Speicherverwaltung selbst als neue statisch gebundene Methode allocate().Mit den Trennern %− und %+ kennt ooc die Bindung jeder Methode, und der

Report selectors kann erweitert werden, um die oben gezeigten Selektoren zu ge-

nerieren. Andere Reports vereinbaren Selektoren in der Schnittstellendatei, Ober-

klassen-Selektoren und das Layout der Metaklassen-Beschreibung in der Repräsen-

tierungsdatei, generieren die Schleife im Metaklassen-Konstruktor, die Selektor,

Markierung und Methode erkennt und in die Klassenbeschreibung einträgt, und

schließlich die Initialisierungsfunktionen für die Klassen- und Metaklassen-Beschrei-

bungen. Alle diese Reports müssen erweitert werden. Zum Beispiel werden im

Report −h in h.rep die Deklarationen für dynamisch gebundene Methoden generiert:

`{%- `%header ; `n `}n

Eine neue Schleife fügt die Deklarationen von Klassenmethoden hinzu:

`{%+ `%header ; `n `}n

`{+ ist eine Schleife über die Klassenmethoden der aktuellen Klasse.

Wenn wir new() und delete() über Selektoren aufrufen, müssen wir die Metho-

den selbst für die Klasse Object in Object.dc implementieren:

% Object new {%casts

return ctor(allocate(self), app);}

new() erzeugt die Speicherfläche für ein neues Objekt und ruft den zuständigen

Konstruktor auf, um es zu initialisieren. allocate() enthält fast allen Code, der

früher in new() stand. allocate() beschafft dynamischen Speicher und installiert

den Zeiger auf die Klassenbeschreibung, damit die dynamische Bindung von ctor()in new() korrekt funktioniert:

Page 153: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

143___________________________________________________________________________11.4 Programmierpraxis — Eine Dialogsprache mit Klasse(n)

% allocate {struct Object * object;

%castsassert(self -> size);object = calloc(1, self -> size);assert(object);object -> magic = MAGIC;object -> class = self;return object;

}

delete() ruft wie vorher den Destruktor dtor() auf und übergibt das Resultat an

free():% Object delete {%casts

free(dtor(self));}

Immer wenn wir neue Methoden für Object einführen, die durch Selektoren aufge-

rufen werden, dürfen wir nicht vergessen, sie von Hand in die Klassenbeschreibun-

gen in Object.dc einzutragen. Hier ist zum Beispiel _Object:static const struct Class _Object = {

{ MAGIC, & _Class },"Object", & _Object, sizeof(struct Object),{ "", (Method) 0, (Method) Object_ctor },...{ "delete", (Method) delete,(Method) Object_delete },...{ "", (Method) 0, (Method) Object_new },

};

11.4 Programmierpraxis — Eine Dialogsprache mit Klasse(n)Nachdem wir die Speicherlecks stopfen können, bauen wir jetzt die Dialogsprache

in unsere Klassenhierarchie ein. Zuerst müssen wir die Beschreibungen aus dem

fünften Kapitel zur Hierarchie hinzufügen.

NodeDer grundsätzliche Baustein für einen Ausdrucksbaum ist die abstrakte Basisklasse

Node. Eine Number ist eine Node, die eine Gleitkommakonstante enthält:

// new(Number(), Wert)

% NodeClass Number: Node {double value;

%}

Unser Baum kann wachsen, wenn wir Knoten mit Unterbäumen haben. Monad hat

nur einen Unterbaum, Dyad hat zwei:

% NodeClass Monad: Node {void * down;

%}

Page 154: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

144___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

%prot#define down(x) (((struct Monad *)(x)) -> down)

% NodeClass Dyad: Node {void * left;void * right;

%}%prot#define left(x) (((struct Dyad *)(x)) -> left)#define right(x) (((struct Dyad *)(x)) -> right)

Eigentlich sollten nur die Konstruktoren der Knoten die Komponenten .down, .leftund .right füllen, aber falls wir einen Baum kopieren, muß eine Unterklasse die Zei-

ger vielleicht doch modifizieren.

Wir verwenden einzelne Unterbäume für zwei völlig verschiedene Zwecke.

Unary repräsentiert einen Operator wie etwa ein Minuszeichen:

// new(Minus(), Unterbaum)

% NodeClass Unary: Monad {%}

% NodeClass Minus: Unary {%}

Val holt den Wert eines Symbols aus der Symboltabelle. Eine Art von Val ist Glo-bal, ein Knoten, der auf Var oder Const in der Symboltabelle zeigt und seinen Wert

von dort holt. Wenn wir noch benutzerdefinierte Funktionen implementieren wol-

len, verwenden wir Parm, um den Wert des einzigen Parameters zu holen.

// new(Global(), const-oder-var)// new(Parm(), fun)

% NodeClass Val: Monad {%}

% NodeClass Global: Val {%}

% NodeClass Parm: Val {%}

Wir werden Symboltabelleneinträge von einer Basisklasse Symbol ableiten, die

nichts mit Node zu tun hat. Deshalb benötigen wir Val und seine Unterklassen,

denn wir können einen Ausdrucksbaum nicht mehr länger direkt auf ein Symbol-Objekt zeigen lassen, das die Methode exec() nicht versteht.

Es gibt viele Knoten mit zwei Unterbäumen. Add, Sub, Mult und Div verknüp-

fen die Werte ihrer Unterbäume. Es wird einfacher, wenn wir Binary als gemeinsa-

me Basisklasse einführen:

// new(Add(), linker-Unterbaum, rechter-Unterbaum)...

% NodeClass Binary: Dyad {%}

% NodeClass Add: Binary {%}...

Page 155: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

145___________________________________________________________________________11.4 Programmierpraxis — Eine Dialogsprache mit Klasse(n)

So wie Val zum Zugriff auf die Werte von Symbolen verwendet wird, verknüpft Refein Symbol und einen Ausdruck: Assign zeigt auf ein Var-Symbol und speichert

dort den Wert des rechten Unterbaums; Builtin zeigt auf ein Math-Symbol, das den

Wert einer Bibliotheksfunktion berechnet und den Wert des rechten Unterbaums

als Argument verwendet; User zeigt auf ein Fun-Symbol, das den Wert einer benut-

zerdefinierten Funktion mit dem Wert des rechten Unterbaums als Argument be-

rechnet.

// new(Assign(), var, rechter-Unterbaum)// new(Builtin(), math, Argument-Unterbaum)// new(User(), fun, Argument-Unterbaum)

% NodeClass Ref: Dyad {%}

% NodeClass Assign: Ref {%}

% NodeClass Builtin: Ref {%}

% NodeClass User: Ref {%}

Die Methoden für die Unterklassen von Node können so ziemlich aus dem fünften

Kapitel kopiert werden. Sie müssen nur geringfügig adaptiert werden. Die folgende

Tabelle zeigt, wie die verschiedenen Methoden in Node und die Unterklassen ge-

bunden werden:

KLASSEN DATEN METHODEN

Node siehe untenNumber value ctor, execMonad down ctor

Val execGlobalParm

Unary dtorMinus exec

Dyad left, right ctorRef dtor

Assign execBuiltin execUser exec

Binary dtorAdd execSub execMult execDiv exec

Wir geben hier zwar das Prinzip auf, daß Konstruktoren und Destruktoren balanciert

sein sollen, aber es gibt einen guten Grund: Die Destruktoren schicken delete() an

ihre Unterbäume. Das ist akzeptabel, wenn wir einen Ausdrucksunterbaum freige-

Page 156: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

146___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

ben, aber wir dürfen delete() natürlich nicht in die Symboltabelle schicken. Val und

Ref wurden genau deshalb eingeführt, um die Destruktoren zu konzentrieren.

Bis hierher sieht es nicht so aus, als ob wir Global und Parm unterscheiden

müssen. Je nach Repräsentierung ihrer Symbole müssen wir jedoch verschiedene

Versionen von exec() implementieren. Indem wir die Unterklassen einführen, be-

halten wir uns diese Entscheidung noch vor.

SymbolWir haben die nötigen Knoten dadurch entdeckt, daß wir die möglichen Ausdrucks-

bäume betrachtet haben. Beim Entwurf der Knoten finden wir die meisten Symbo-

le, die wir brauchen. Symbol ist die abstrakte Basisklasse aller Symbole, die in ei-

ne Symboltabelle eingetragen und nach Namen gefunden werden können.

Reserved ist ein reserviertes Wort:

// new(Reserved(), "name", lex)

% Class Reserved: Symbol {%}

Var ist ein Symbol mit einem Gleitkommawert. Global zeigt auf ein Var-Objekt und

benützt value(), um den aktuellen Wert zu erhalten; Assign benützt setvalue(), um

einen neuen Wert zu hinterlegen:

// new(Var(), "name", VAR)

% Class Var: Symbol {double value;

%double value (const _self);double setvalue (_self, double value);

%}

Const ist eine Unterklasse von Var mit einem anderen Konstruktor:

// new(Const(), "name", CONST, Wert)

% Class Const: Var {%}

Wenn wir Const als Unterklasse von Var realisieren, vermeiden wir, daß im umge-

kehrten Fall setvalue() aus der Unterklasse die Komponente .value in der Oberklas-

se manipulieren müßte, und daß wir ein Var-Objekt bei der Konstruktion initialisie-

ren müssen. Wir werden Const syntaktisch davor schützen, als Ziel in einem

Assign-Knoten vorzukommen.

Math repräsentiert eine Bibliotheksfunktion. Builtin verwendet mathvalue(),um einen Argumentwert zu liefern und den Funktionswert als Resultat zu bekom-

men:

// new(Math(), "name", MATH, Funktionsname)

typedef double (* function) (double);

% Class Math: Symbol {function fun;

%double mathvalue (const _self, double value);

%}

Page 157: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

147___________________________________________________________________________11.4 Programmierpraxis — Eine Dialogsprache mit Klasse(n)

Fun repräsentiert schließlich eine benutzerdefinierte Funktion mit einem einzigen

Parameter. Dieses Symbol zeigt auf einen Ausdrucksbaum, der mit setfun() hinter-

legt und ersetzt werden kann. Ein User-Knoten verwendet funvalue(), um die

Funktion zu bewerten:

// new(Fun(), "name", FUN)

% Class Fun: Var {void * fun;

%void setfun (_self, Node @ fun);double funvalue (_self, double value);

%}

Wir ignorieren Rekursionsprobleme und vereinbaren Fun als Unterklasse von Var,so daß wir den Argumentwert mit setvalue() speichern können, und wir generieren

einen Parm-Knoten, wenn der Wert des Parameters in einem Ausdruck gebraucht

wird. Hier ist die Klassenhierarchie für Symbol:

KLASSEN DATEN METHODEN

Symbol name, lex siehe untenReserved deleteVar value % value, setvalue

Const ctor, deleteFun fun % setfun, funvalue

Math fun ctor, delete% mathvalue

Wieder kann fast aller Code aus dem fünften Kapitel kopiert werden. Wir müssen

nur wenig für die Klassenhierarchie ändern. Const und Math sollten nie gelöscht

werden, deshalb schützen wir sie mit trivialen Methoden:

% : Const delete { // respondTo sieht delete nicht}

Die einzige neue Idee sind benutzerdefinierte Funktionen, die in der Klasse Fun im-

plementiert werden:

% Fun setfun {%casts

if (self -> fun)delete(self -> fun);

self -> fun = fun;}

Wenn wir eine Funktionsdefinition ersetzen, müssen wir zuerst den alten Aus-

drucksbaum löschen, falls es einen gibt.

% Fun funvalue {%casts

if (! self -> fun)error("undefined function");

setvalue(self, value); // Argument fuer Parameterreturn exec(self -> fun);

}

Page 158: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

148___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

Um den Funktionswert zu berechnen, importieren wir den Argumentwert, so daß

Parm diesen Wert mit value() als Parameterwert abholen kann. exec() kann dann

den Funktionswert aus dem Ausdrucksbaum berechnen.

SymtabWir könnten versuchen, eine Symboltabelle aus List zu entwickeln, aber die Funkti-

on binary() zur binären Suche, die wir im fünften Kapitel verwendet haben, muß auf

Vektoren angewendet werden, und wir benötigen nur die Methoden screen() und

install():// new(Symtab(), minimale-Dimension)

#include <stddef.h>

% Class Symtab: Object {const void ** buf; // const void * buf [dim]size_t dim; // aktuelle Pufferlaengesize_t count; // # Elemente im Puffer

%void install (_self, const Symbol @ entry);Symbol @ screen (_self, const char * name, int lex);

%}

Der Vektor wird wie für List angelegt:

% Symtab ctor {struct Symtab * self = super_ctor(Symtab(), _self, app);

if (! (self -> dim = va_arg(* app, size_t)))self -> dim = 1;

self -> buf = malloc(self -> dim * sizeof(void *));assert(self -> buf);return self;

}

search() ist eine interne Funktion, die mit binary() nach einem Symbol mit einem

bestimmten Namen sucht oder die diesen Namen selbst in die Symboltabelle ein-

trägt:

static void ** search (struct Symtab * self, const char ** np){

if (self -> count >= self -> dim){ self -> buf = realloc(self -> buf,

(self -> dim *= 2) * sizeof(void *));assert(self -> buf);

}return binary(np, self -> buf, & self -> count,

sizeof(void *), cmp);}

Dies ist eine interne Funktion, deshalb benützen wir einen kleinen Trick: binary()sucht zwar nach einem Symbol-Objekt, aber wenn keines gefunden wird, trägt

binary() temporär nur den String bei *np und kein neues Symbol-Objekt in die Ta-

belle ein. cmp() vergleicht die Zeichenkette mit einem Symbol — wenn wir eine

Page 159: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

149___________________________________________________________________________11.4 Programmierpraxis — Eine Dialogsprache mit Klasse(n)

Klasse wie Atom für Zeichenketten verwenden würden, könnten wir cmp() mit

differ() implementieren:

static int cmp (const void * _key, const void * _elt){ const char * const * key = _key;

const void * const * elt = _elt;

return strcmp(* key, name(* elt));}

name() ist eine Methode in der Klasse Symbol, die den Namen eines Symbols lie-

fert. Wir vergleichen den Namen mit der Zeichenkette für search() und erzeugen

erst dann ein Symbol-Objekt, wenn wir wissen, daß die Suche erfolglos blieb.

Nachdem wir so in unserer Tabelle suchen und eintragen können, sind die ei-

gentlichen Methoden der Klasse Symtab sehr leicht zu implementieren. install()wird mit einem zweiten Argument aufgerufen, das mit new() erzeugt wird. Damit

können wir beliebige Symbol-Objekte in die Symboltabelle eintragen:

% Symtab install {const char * nm;void ** pp;

%castsnm = name(entry);pp = search(self, & nm);if (* pp != nm) // entry gefunden

delete(* pp);* pp = (void *) entry;

}

install() ersetzt auch ein Symbol in der Tabelle.

% Symtab screen {void ** pp;

%castspp = search(self, & name);if (* pp == name) // name eingetragen{ char * copy = malloc(strlen(name) + 1);

assert(copy);* pp = new(Symbol(), strcpy(copy, name), lex);

}return * pp;

}

screen() findet entweder einen Eintrag mit dem gesuchten Namen oder trägt ein

neues Symbol-Objekt mit einem dynamisch gespeicherten Namen ein. Wenn wir

uns später dafür entscheiden, daß der Tabelleneintrag ein Objekt aus einer Unter-

klasse von Symbol sein soll, können wir install() aufrufen und einen Eintrag in der

Tabelle ersetzen. Das ist zwar ein bißchen ineffizient, aber wir benötigen dann kei-

ne neuen Methoden für die Symboltabelle.

Page 160: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

150___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

Die abstrakten BasisklassenSymbol ist die Basisklasse für Einträge in der Symboltabelle. Ein Symbol enthält

einen Namen und einen Wert für den Erkenner, die beide durch den Konstruktor

eingetragen werden:

Symbol.d// new(Symbol(), "name", lex) "name" nicht aendern

% Class Symbol: Object {const char * name;int lex;

%const char * name (const _self);int lex (const _self);

%}

Symbol.dc% Symbol ctor {

struct Symbol * self = super_ctor(Symbol(), _self, app);

self -> name = va_arg(* app, const char *);self -> lex = va_arg(* app, int);return self;

}

In der Klasse Symbol gehen wir davon aus, daß bereits dafür gesorgt wurde, daß

der Name des Symbols einigermaßen permanent gespeichert wurde: Entweder ist

der Name eine statische Zeichenkette, oder der Name muß dynamisch gespeichert

werden, bevor dafür ein Symbol-Objekt konstruiert wird. Symbol speichert den

Namen nicht und gibt ihn auch nicht frei. Wenn screen() einen Namen dynamisch

speichert, und wenn wir Symbole mit install() ersetzen, dann können wir einfach

den Namen vom vorhergehenden Symbol übernehmen, das von install() gelöscht

wird, um unnützes Kopieren im dynamischen Speicher zu vermeiden. Eine Klasse

wie Atom wäre allerdings eine wesentlich bessere Strategie.

Die wirklich interessante Klasse ist aber Node, die abstrakte Basisklasse für alle

Teile eines Ausdrucksbaums. Alle neuen Knoten werden in einer linearen Liste ge-

sammelt, damit wir sie im Fehlerfall einsammeln können:

Node.d% NodeClass: Class Node: Object {

void * next;%

void sunder (_self);%-

double exec (const _self);%+

void reclaim (const _self, Method how);%}

Page 161: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

151___________________________________________________________________________11.4 Programmierpraxis — Eine Dialogsprache mit Klasse(n)

Node.dcstatic void * nodes; // verkettet alle Knoten

% Node new {struct Node * result =

cast(Node(), super_new(Node(), _self, app));

result -> next = nodes, nodes = result;return (void *) result;

}

Nach Webster bedeutet sunder ‘‘endgültig und vollständig, oder mit Gewalt, tren-

nen’’, und das ist genau das, was wir tun:

% Node sunder {%casts

if (nodes == self) // erster Knotennodes = self -> next;

else if (nodes) // anderer Knoten{ struct Node * np = nodes;

while (np -> next && np -> next != self)np = np -> next;

if (np -> next)np -> next = self -> next;

}self -> next = 0;

}

Bevor wir einen Knoten löschen, entfernen wir ihn aus der Kette:

% Node delete {%casts

sunder(self);super_delete(Node(), self);

}

Speicherlecks stopfenNormalerweise ruft der Erkenner in parse.c selbst delete() auf, wenn ein Ausdruck

fertig bearbeitet wurde:

if (setjmp(onError)){ ++ errors;

reclaim(Node(), delete);}

while (gets(buf))if (scan(buf)){ void * e = stmt();

if (e){ printf("\t%g\n", exec(e));

delete(e);}

}

Page 162: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

152___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

Wenn etwas schiefgeht und error() aufgerufen wird, dient reclaim() dazu, alle Kno-

ten in der Kette an delete() zu übergeben:

% Node reclaim {%casts

while (nodes)how(nodes);

}

Damit wird das Speicherleck eliminiert, das am Anfang dieses Kapitel geschildert

wurde — MallocDebug findet keine Lecks, weder unmittelbar nach einem Fehler,

noch später. Für Testzwecke können wir nach einem Fehler

reclaim(Node, sunder);

ausführen und mit MallocDebug demonstrieren, daß wir wirklich Knoten verloren

haben.

Die Eleganz des Verfahrens liegt in der Tatsache, daß der ganze Mechanismus

in der Basisklasse Node verkapselt ist und vom ganzen Ausdrucksbaum geerbt

wird. Mit Klassenmethoden können wir new() für einen Unterbaum der Klassen-

hierarchie ersetzen. Wenn wir new() genau für Knoten, aber nicht für Symbole

oder die Symboltabelle ersetzen, können wir defekte Ausdrücke freigeben, ohne Va-

riablen, Funktionen oder den Rest zu beschädigen.

Wir haben reclaim() als Klassenmethode vereinbart. Wir haben die Methode

zwar in keiner Unterklasse von Node ersetzt, aber so bleibt Platz für Erweiterungen.

Bei reclaim() kann man wählen, was auf die Kette angewendet werden soll. Bei

Fehlern ist das delete(), aber wenn wir einen Ausdruck als benutzerdefinierte Funk-

tion in einem Fun-Symbol hinterlegen, müssen wir sunder() auf die Kette anwen-

den, damit der nächste Fehler nicht den Ausdruck eliminiert, der dann in der Sym-

boltabelle gespeichert ist. Wenn eine Funktion ersetzt wird, löscht setfun() den al-

ten Ausdruck, und dabei verwendet delete() ebenfalls sunder() — deshalb verlangt

sunder() nicht unbedingt, daß sein Argument auf der Kette aller Knoten vorkommen

muß.

11.5 ZusammenfassungKlassenmethoden werden nur auf Klassenbeschreibungen und nicht auf andere Ob-

jekte angewendet. Wir benötigen mindestens eine Klassenmethode: new() er-

zeugt Objekte aus einer Klassenbeschreibung.

Wie andere Methoden können auch Klassenmethoden statisch oder dynamisch

gebunden werden, aber die Syntax von ooc erlaubt statische Bindung nur für die

Klassenmethoden der Wurzel-Metaklasse. In diesem Kapitel haben wir deshalb den

Begriff Klassenmethode nur für eine Methode mit dynamischer Bindung verwendet,

die auf eine Klassenbeschreibung angewendet wird.

Da es relativ wenige Klassenbeschreibungen gibt, können wir die dynamische

Bindung für eine Klassenmethode dadurch herstellen, daß wir sie in der Klassenbe-

schreibung selbst speichern, auf die die Methode angewendet wird. Das hat zwei

Vorteile: Wir können Klassenmethoden für eine Unterklasse ersetzen, ohne daß wir

Page 163: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

153___________________________________________________________________________11.5 Zusammenfassung

dazu eine neue Metaklasse einführen müssen, und unser grundsätzliches Schema

muß nicht erweitert werden, wonach Objekte auf Klassenbeschreibungen und Klas-

senbeschreibungen auf ihre eigenen Beschreibungen zeigen, und wo die letzteren

alle in struct Class gespeichert werden, das heißt, alle dann auf Class zeigen und

die Klassenhierarchie ordentlich abschließen.

Wenn wir new() als Klassenmethode für Object und nicht mit statischer Bin-

dung in Class vereinbaren, können wir new() für Unterbäume der Klassenhierarchie

ersetzen. Damit können wir die Speicherverwaltung verfolgen, Speicher gemein-

sam nutzen etc. ooc kann keine Datenkomponenten für eine Klassenbeschreibung

vereinbaren. Wenn man dies auch noch ermöglicht, kann eine Klassenmethode lo-

kale Daten besitzen, die dann für die ganze Klasse gemeinsam zur Verfügung ste-

hen. Damit kann man zum Beispiel die Objekte in einer Klasse zählen. static ver-

einbarte Variablen in einer Quelldatei sind nicht ganz dasselbe, denn sie existieren

nur einmal für die Klasse und alle ihre Unterklassen.

new() und ein Konstruktor können sich die Arbeit teilen. Die Versuchung ist

groß, alles in new() zu erledigen und den Konstruktor leer zu lassen, aber dann kön-

nen Invarianten, die normalerweise ein Konstruktor etabliert, verletzt werden, wenn

new() ersetzt wird. Analog ist ein Konstruktor zwar in der Lage, den Speicherbe-

reich auszutauschen, den er von new() erhält — dies wurde in der Implementierung

von Atom im Abschnitt 2.6 vorgeführt — aber es ist schwierig, für den ausge-

tauschten Speicherbereich einen vernünftigen Kreislauf aufrechtzuerhalten.

Als Faustregel sollten Klassenmethoden wie new() nur eine Speicherverwal-

tungsfunktion mit einem Konstruktor verbinden und selbst keine Initialisierungen

vornehmen. Speicherverwaltungsfunktionen wie allocate() sollten den Zeiger auf

die Klassenbeschreibung initialisieren — zu viel kann schrecklich schiefgehen, wenn

sie das nicht tun. Löschfunktionen wie delete() sollten den Destruktor die Ressour-

cen wiedergewinnen lassen, die der Konstruktor und die Aktivitäten des Objekts ge-

sammelt haben, und selbst nur die leere Speicherfläche an eine Funktion wie free()übergeben:

allocate()

anObject

ctor()

aThing

new() delete()

aThing

dtor()

anObject

free()

aThing

Hier gibt es Symmetrie: allocate() und free() arbeiten mit der gleichen Speicher-

fläche; nach Voreinstellung übergibt sie new() an ihren Konstruktor und delete() an

ihren Destruktor, und Konstruktor und Destruktor beschäftigen sich nur mit Res-

sourcen, die im Objekt repräsentiert werden. new() und delete() sollten nur ersetzt

werden, um den Speicherfluß von allocate() zu free() abzuändern.

Page 164: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

154___________________________________________________________________________11 Klassenmethoden — Lecks in der Speicherverwaltung

11.6 ÜberlegungenFür den Erkenner in ooc ist es belanglos, ob Klassenmethoden in der Klassenbe-

schreibungsdatei vor oder nach dynamisch gebundenen Methoden vereinbart wer-

den, das heißt, ob %+ vor oder nach %− steht. Es gibt allerdings einen Gesichts-

punkt, warum die in diesem Kapitel beschriebene Anordnung zu bevorzugen ist.

Warum kann man die Trenner nicht beliebig wiederholen lassen und beide Arten

von Methoden einfach mischen?

Wenn wir delete() mit dynamischer Bindung implementieren, ergibt sich ein ge-

waltiger Unterschied. Was kann man nicht mehr an delete() übergeben?

Es ist keine gute Idee, value() in die abstrakte Basisklasse Symbol zu verlegen

und dort dynamisch zu binden. mathvalue() gehört zu einem Math-Symbol und

benötigt ein Funktionsargument, value() bezieht sich auf ein Var- oder Const-Symbol und kann mit einem Argument nichts anfangen. Sollten wir variable Para-

meterlisten verwenden?

Wir können feststellen, ob benutzerdefinierte Funktionen rekursiv verwendet

werden. Wir könnten Namen wie $1 verwenden, um Funktionen mit mehr als ei-

nem Parameter zu realisieren. Wir könnten auch Parameternamen einführen, die

globale Variablen verdecken.

Wenn wir einen generischen Zeiger zur Datenfläche von Class in Object.d hin-

zufügen, können Klassenmethoden dort eine Kette mit privaten Datenflächen ein-

hängen. Damit könnte man zum Beispiel Objekte in einer Klasse zählen oder Ob-

jektlisten für einzelne Klassen führen.

Page 165: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

155___________________________________________________________________________

12Persistente Objekte

Datenstrukturen speichern und laden

Im Abschnitt 6.3 wurde die dynamisch gebundene Methode puto() in der KlasseObject eingeführt, die ein Objekt in einem Ausgabestrom darstellt. Zum Beispielproduziert

void * anObject = new(Object());...puto(anObject, stdout);

ungefähr folgende Standard-Ausgabe:

Object at 0x5410Wenn wir puto() für jede Klasse in einer Hierarchie implementieren, können wir je-des Objekt darstellen. Wenn die Ausgabe gut genug entworfen ist, sollten wir dieObjekte auch wiederherstellen können, das heißt, Objekte können dann in Dateiengeparkt werden und von einem Aufruf einer Applikation bis zum nächsten existie-ren. Wir nennen solche Objekte persistent. Objekt-orientierte Datenbanken beste-hen aus persistenten Objekten und Mechanismen, um sie nach Namen oder Inhaltzu suchen.

12.1 Ein BeispielUnsere Dialogsprache enthält eine Symboltabelle mit Variablen, Konstanten undFunktionen. Konstanten und mathematische Funktionen sind vordefiniert, aber wirverlieren alle Variablenwerte und die benutzerdefinierten Funktionen, wenn wir dieAusführung beenden. Als realistisches Beispiel für den Einsatz von persistentenObjekten fügen wir zwei Anweisungen zur Dialogsprache hinzu: save speichert ei-nige oder alle Variablen und Funktionsdefinitionen in Dateien, load lädt sie wieder.

$ valuedef sqr = $ * $def one = sqr(sin($)) + sqr(cos($))let n = one(10)

1save

Bei diesem Aufruf von value definieren wir die Funktionen sqr() und one(), um diebekannte Gleichung sin2x + cos2x ≡ 1 nachzuprüfen. Außerdem erzeugen wir dieVariable n mit Wert eins. Ohne Argumente speichert save die drei Definitionen ineiner Datei value.stb.

$ valueloadn + one(20)

2Wenn wir value wieder starten, können wir load verwenden, um die Definitionen zuladen. Der Ausdruck demonstriert, daß wir den Wert der Variablen und beide Funk-tionsdefinitionen wiederherstellen.

Page 166: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

156___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

save wird im Erkenner in der Funktion stmt() genau wie let oder def implemen-tiert. Ohne Argument müssen wir alle Variablen und Funktionen speichern, deshalbübergeben wir das Problem einfach an Symtab:

#define SYMTABFILE "value.stb"#define SYMBOLFILE "%s.sym"static void * stmt (void){ void * sym, * node;

switch (token) {...

case SAVE:if (! scan(0)) /* ganze Symboltabelle */{ if (save(table, 0, SYMTABFILE))

error("cannot save symbol table");}else /* Liste von Symbolen */

do{ char fnm [BUFSIZ];

sprintf(fnm, SYMBOLFILE, name(symbol));if (save(table, symbol, fnm))

error("cannot save %s", name(symbol));} while (scan(0));

return 0;Mit einer komplizierteren Syntax könnte man einen Dateinamen bei save angeben.Zur Vereinfachung definieren wir SYMTABFILE und SYMBOLFILE: Die ganze Symbol-tabelle wird in value.stb gespeichert und ein einzelnes Symbol wie one würde inone.sym abgelegt. Die Anwendung kontrolliert den Dateinamen, das heißt, er wirdin parse.c konstruiert und an save() übergeben.

Symtab.d% Class Symtab: Object {

...%

...int save (const _self, const Var @ entry, const char * fnm);int load (_self, Symbol @ entry, const char * fnm);

%}Symtab.dc

% Symtab save {const struct Symtab * self = cast(Symtab(), _self);FILE * fp;if (entry) // ein Symbol{ if (! respondsTo(entry, "move"))

return EOF;if (! (fp = fopen(fnm, "w")))

return EOF;puto(entry, fp);

}

Page 167: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

157___________________________________________________________________________12.1 Ein Beispiel

Ein einzelnes Symbol wird als entry übergeben. Undefinierte Symbole, Konstantenoder mathematische Funktionen zu speichern ist sinnlos, deshalb speichern wir nurSymbole, die die Methode move() besitzen. Wir werden unten sehen, daß dieseMethode dazu dient, ein Symbol aus einer Datei zu laden. save() eröffnet Zugriffauf die Ausgabedatei und läßt puto() die eigentliche Arbeit tun. Alle Symbole insge-samt zu speichern ist fast so leicht:

else // ganze Tabelle{ int i;

if (! (fp = fopen(fnm, "w")))return EOF;

for (i = 0; i < self -> count; ++ i)if (respondsTo(self -> buf[i], "move"))

puto(self -> buf[i], fp);}return fclose(fp); // 0 oder EOF

}save() ist als Methode in Symtab definiert, denn wir müssen eine Schleife über dieElemente in .buf[] programmieren. Der Test, ob ein Symbol wirklich in einer Dateiabgespeichert werden soll, findet zwar nur in save() statt, aber Symtab sollte nichtwissen, welche Art von Symbolen wir haben. Deshalb basieren wir die Entschei-dung zu speichern auf das Vorhandensein einer Methode move() und nicht auf Mit-gliedschaft in manchen Unterklassen von Symbol.

Es sieht so aus, als ob Laden völlig symmetrisch zu Speichern verlaufen sollte.Wieder überlegen wir uns den Dateinamen in parse.c und lassen load() die eigentli-che Arbeit tun:

case LOAD:if (! scan(0)) /* ganze Symboltabelle */{ if (load(table, 0, SYMTABFILE))

error("cannot load symbol table");}else /* Liste von Symbolen */

do{ char fnm [BUFSIZ];

sprintf(fnm, SYMBOLFILE, name(symbol));if (load(table, symbol, fnm))

error("cannot load %s", name(symbol));} while (scan(0));

reclaim(Node(), sunder);return 0;

Leider ist aber load() total verschieden von save(). Es gibt zwei Gründe: Wir solltenwenigstens versuchen, unser Programm vor jemand zu schützen, der mit Datei-namen oder -inhalten herumspielt, und während save() ein beliebiges Symbol in derSymboltabelle einfach mit puto() anzeigen kann, ist es ziemlich wahrscheinlich, daßwir bei save() Symbole in die Tabelle eintragen oder dort ändern müssen. Persi-stente Objekte zu laden ist fast dasselbe, als sie ursprünglich anzulegen und zu kon-struieren.

Page 168: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

158___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

Wir betrachten load() Schritt für Schritt. Wenn ein einziges Symbol geladenwerden soll, ist sein Name bereits in der Symboltabelle, denn er mußte bei derload-Anweisung angegeben werden. Dabei betrachten wir entweder ein undefi-niertes Symbol-Objekt, oder das Symbol besitzt die Methode move():

% Symtab load {struct Symtab * self = cast(Symtab(), _self);const char * target = NULL;FILE * fp;int result = EOF;void * in;if (entry)

if (isOf(entry, Symbol())|| respondsTo(entry, "move"))

target = name(entry);else

return EOF;Wenn ein Wert für entry übergeben wurde, sollten wir ihn frühzeitig überprüfen, da-mit wir nicht ganz umsonst arbeiten. Als nächstes greifen wir auf die Datei zu undlesen so viele Symbole ein wie möglich:

if (! (fp = fopen(fnm, "r")))return EOF;

while (in = retrieve(fp))...

if (! target && feof(fp))result = 0;

fclose(fp);return result;

}Wenn wir nicht ein bestimmtes Symbol, eben entry, suchen, sind wir zufrieden,wenn wir das Dateiende erreichen. retrieve() wird im Abschnitt 12.4 besprochen— diese Funktion liefert ein Objekt aus einem Eingabestrom.

Der Körper der while-Schleife bearbeitet ein Symbol auf einmal. Wir haben einernstes Problem, wenn das eingelesene Objekt nichts von move() weiß, denn dannkann der Strom ganz bestimmt nicht von save() geschrieben worden sein. Wenndas passiert, beenden wir die Schleife und lassen load() den Wert EOF liefern. An-dernfalls, wenn wir nach einem bestimmten Symbol in entry suchen, überspringenwir alle Symbole mit anderen Namen.

{ const char * nm;void ** pp;if (! respondsTo(in, "move"))

break;if (target && strcmp(name(in), target))

continue;

Page 169: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

159___________________________________________________________________________12.1 Ein Beispiel

parse.c hätte zwar dafür sorgen sollen, daß eine Datei entweder das eine ge-wünschte Symbol oder eine komplette Symboltabelle enthält, aber der Aufruf vonstrcmp() schützt uns vor umbenannten oder modifizierten Dateien.

Wir können jetzt das eingelesene Symbol in die Symboltabelle bringen. Dafürist load() eine Methode in der Klasse Symtab. Der Vorgang ist recht ähnlich zuscreen(): Wir gehen davon aus, daß retrieve() den Namen dynamisch gespeicherthat, und wir suchen den Namen mit search() in der Tabelle. Wenn wir den vonretrieve() gespeicherten Namen wiederentdecken, haben wir gerade ein neuesSymbol eingelesen, das wir einfach in die Tabelle einfügen können:

nm = name(in);pp = search(self, & nm);if (* pp == nm) // noch nicht in Tabelle

* pp = in;Höchstwahrscheinlich hat load aber den Namen eines neuen Symbols bekommen,das geladen werden soll. In diesem Fall ist der Name schon in einem undefiniertenSymbol-Objekt in der Tabelle dynamisch gespeichert. Wir entfernen es total undfügen das eingelesene Symbol an seiner Stelle ein.

else if (isA(* pp, Symbol()))// noch nicht definiert

{ nm = name(* pp), delete(* pp), free((void *) nm);* pp = in;

} // koennte target freigeben, aber dann exitWenn wir bis hierher kommen, müssen wir uns mit einem existenten Symbol be-schäftigen, das heißt, eine Variable erhält einen neuen Wert, oder eine Funktionwird umdefiniert. Wir ersetzen aber nur ein Symbol, das die Methode move()kennt, um uns dagegen zu schützen, daß jemand den Inhalt unserer Eingabedateigeändert hat.

else if (! respondsTo(* pp, "move")){ nm = name(in); delete(in); free((void *) nm);

continue; // sollte nicht passieren}else{ move(* pp, in);

delete(in), free((void *) nm);}if (target){ result = 0;

break;}

}Wenn wir das gesuchte Symbol entry gefunden haben, können wir die Schleife ver-lassen.

Wir müssen sehr vorsichtig sein, daß wir kein existentes Symbol ersetzen,denn ein Ausdruck zeigt vielleicht schon darauf. Deshalb haben wir move() als dy-namisch gebundene Methode eingeführt, die einen Wert von einem Symbol zu ei-nem anderen transferiert.

Page 170: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

160___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

Symbol.d% VarClass: Class Var: Symbol {

...%-

void move (_self, _from);%}

Symbol.dc% Var move {%casts

setvalue(self, from -> value);}% : Const move { // respondTo sieht move nicht}% Fun move {%casts

setfun(self, from -> fun), from -> fun = 0;}

Var und Fun erlauben move(), aber Const nicht. move() gleicht einer ‘‘flachen Ko-pie’’ (shallow copy): Wenn ein Fun-Symbol auf einen Ausdrucksbaum zeigt, wirdzwar der Zeiger, aber nicht der ganze Ausdruck kopiert. Tatsächlich setzt move()den Quellzeiger auf Null, damit das ursprüngliche Fun-Symbol freigegeben werdenkann, ohne daß der übertragene Ausdrucksbaum zerstört wird.

12.2 Objekte speichern — putoputo() ist eine Methode in der Klasse Object, die die Repräsentierung eines Ob-jekts auf einen FILE-Zeiger ausgibt und die Anzahl der geschriebenen Bytes als Re-sultat liefert.

Object.d% Class Object {

...%-

int puto (const _self, FILE * fp); // ausgebenObject.dc

% Object puto {%casts

class = classOf(self);return fprintf(fp, "%s at %p\n", class -> name, self);

}Wie wir im Abschnitt 12.3 sehen werden, muß die Ausgabe unbedingt mit demKlassennamen des Objekts beginnen. Die Adresse des Objekts müssen wir nichtunbedingt ausgeben.

Zwar darf jede Unterklasse ihre eigene Version von puto() implementieren, aberdie einfachste Lösung besteht darin, daß puto() wie ein Konstruktor funktioniert,

Page 171: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

161___________________________________________________________________________12.2 Objekte speichern — ‘‘puto’’

das heißt, wir leiten die Aufrufe die ganze Oberklassen-Kette aufwärts bis zuObject_puto() und stellen so sicher, daß die Ausgabe mit dem Klassennamen be-ginnt und den Zustand der Instanz von jeder beteiligten Klasse erhält. Damit mußsich jede puto-Methode nur um die Information kümmern, die in ihrer eigenen Klas-se zum Objekt hinzukommt, und nicht um den gesamten Inhalt eines Objekts. Be-trachten wir zum Beispiel Var und Symbol:

% Var puto {int result;

%castsresult = super_puto(Var(), _self, fp);return result + fprintf(fp, "\tvalue %g\n", self -> value);

}% Symbol puto {

int result;%casts

result = super_puto(Symbol(), _self, fp);return result + fprintf(fp, "\tname %s\n\tlex %d\n",

self -> name, self -> lex);}

Hier erhalten wir ungefähr folgende Ausgabe:

Var at 0x50ecb18 Objectname x Symbollex 118value 1 Var

Wir könnten versuchen, den Code zu vereinfachen und auf die int-Variable zu ver-zichten:

% Var puto { // falsch!%casts

return super_puto(Var(), _self, fp)+ fprintf(fp, "\tvalue %g\n", self -> value);

}ANSI-C garantiert jedoch nicht, daß die Operanden eines Operators wie + von linksnach rechts bewertet werden, das heißt, diese Methode könnte bei manchen Sy-stemen die Reihenfolge der Ausgabezeilen durcheinanderbringen.

Für einfache Objekte ist das Format der Ausgabe von puto() leicht zu entwer-fen: Wir geben jede Komponente mit einem geeigneten Format aus, und wir ver-wenden puto() für Zeiger auf andere Objekte — mindestens so lange wir sichersind, daß wir damit in keine endlose Schleife geraten.

Eine sogenannte Container-Klasse, das heißt, eine Klasse, die andere Objekteverwaltet, ist schwieriger. Die Ausgabe muß so entworfen werden, daß sie korrekteingelesen werden kann, insbesondere, wenn eine unbekannte Zahl von Objektengeschrieben und später wiederhergestellt werden muß. Anders als bei save() imAbschnitt 12.1 können wir uns nicht darauf berufen, daß ein Dateiende anzeigt, daßes keine weiteren Objekte gibt.

Page 172: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

162___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

Im allgemeinen Fall müssen wir eine Präfix-Notation verwenden: Entweder wirschreiben die Anzahl der Objekte vor eine Folge von Objekten, oder wir schreibenvor jedes Objekt zum Beispiel ein Pluszeichen und verwenden etwa einen Punkt anStelle des Pluszeichens, um den Schluß der Objektfolge zu markieren. Wir könntenzwar mit ungetc(getc(fp), fp) ein Zeichen vorausschauen, aber wenn wir nur dieAbwesenheit eines bestimmten führenden Zeichens dazu verwenden, eine Folge zuterminieren, würden wir uns leider darauf verlassen, daß andere Objekte unsere Ab-sicht nicht zu Fall bringen.

Die Klasse Fun in unserer Dialogsprache ist eine andere Art von Container-Klas-se: Hier haben wir ein Symbol, das einen Ausdruck enthält, der aus Node-Objektenbesteht. puto() gibt den Ausdrucksbaum in preorder aus, also Knoten vor Unter-bäumen; wenn wir den Grad eines Knotens kennen, können wir den Knoten ausdieser Information leicht wiederherstellen:

% Binary puto {int result;

%castsresult = super_puto(Binary(), self, fp);result += puto(left(self), fp);return result + puto(right(self), fp);

}Die einzige Falle besteht in einer Funktion, die auf andere Funktionen verweist.Wenn wir einfach puto() auf den Verweis anwenden, und wenn wir rekursive Funk-tionen nicht verbieten, können wir leicht steckenbleiben. Die Klassen Ref und Valwurden eingeführt, um Symboltabellenverweise im Ausdrucksbaum zu markieren.Bei einem Verweis schreiben wir nur den Funktionsnamen:

% Ref puto {int result;

%castsresult = super_puto(Ref(), self, fp);result += putsymbol(left(self), fp);return result + puto(right(self), fp);

}Im nächsten Abschnitt wird erklärt, warum wir putsymbol() in parse.c definieren:

int putsymbol (const void * sym, FILE * fp){

return fprintf(fp, "\tname %s\n\tlex %d\n",name(sym), lex(sym));

}Es reicht, wenn wir den Namen des Symbols und den Wert für den Erkenner ausge-ben.

12.3 Objekte füllen — getogeto() ist eine Methode in der Klasse Object, die Information von einem FILE-Zeiger liest und damit ein Objekt füllt. geto() wird auf ein uninitialisiertes Objekt an-gewendet, daher hat diese Funktion eine ganz ähnliche Aufgabe wie ein Konstruk-

Page 173: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

163___________________________________________________________________________12.3 Objekte füllen — ‘‘geto’’

tor. Während aber der Konstruktor ctor() die Information für das neue Objekt ausseiner Argumentliste holt, liest sie geto() aus einem Eingabestrom.

Object.d% Class Object {

...%-: void * geto (_self, FILE * fp); // aus Datei erzeugen

Wir geben hier eine leere Markierung an, denn für ein initialisiertes Objekt darfrespondTo() die Methode geto() nicht finden.

Symbol.dc% Var geto {

struct Var * self = super_geto(Var(), _self, fp);if (fscanf(fp, "\tvalue %lg\n", & self -> value) != 1)

assert(0);return self;

}Var_geto() läßt die Oberklassen-Methoden sich um den Anfang der Informationkümmern und liest selbst nur das ein, was Var_puto() geschrieben hat. Normaler-weise kann man für fprintf() in der puto-Methode und für fscanf() in der geto-Methode das gleiche Format verwenden. Gleitkommawerte zeigen jedoch einesubtile Falle in ANSI-C auf: fprintf() benützt %g um einen double-Wert umzuwan-deln, aber fscanf() benötigt %lg für die umgekehrte Operation. Strings müssennormalerweise dynamisch gespeichert werden:

% Symbol geto {struct Symbol * self = super_geto(Symbol(), _self, fp);char buf [BUFSIZ];if (fscanf(fp, "\tname %s\n\tlex %d\n",

buf, & self -> lex) != 2)assert(0);

self -> name = malloc(strlen(buf) + 1);assert(self -> name);strcpy((char *) self -> name, buf);return self;

}Normalerweise liest geto() genau das, was der zugehörige Aufruf von puto() ge-schrieben hat, und genau wie Konstruktoren rufen beide Methoden ihre Oberklas-sen-Methoden aufwärts bis zu Object auf. Es gibt jedoch einen sehr wichtigen Un-terschied. Wir sahen, daß Object_puto() den Klassennamen und dann eine Adres-se geschrieben hat:

Var at 0x50ecb18 Objectname x Symbollex 118value 1 Var

Page 174: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

164___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

Object_geto() ist die erste Methode, die das Var-Objekt bei der Eingabe füllt. DerKlassenname Var, den puto() geschrieben hat, muß gelesen werden, damit einVar-Objekt angelegt werden kann, bevor geto() aufgerufen wird, um das Objekt zufüllen, das heißt, Object_geto() beginnt erst nach dem Klassennamen zu lesen:

% Object geto {void * dummy;

%castsif (fscanf(fp, " at %p\n", & dummy) != 1)

assert(0);return self;

}Dies ist der einzige Punkt, wo die geto- und puto-Methoden nicht genau symme-trisch sind. Die Variable dummy ist notwendig: Wir könnten sie zwar mit dem For-matelement %*p vermeiden, aber dann könnten wir nicht feststellen, ob die Adres-se wirklich Teil der Eingabe war.

12.4 Objekte laden — retrieveWer liest den Klassennamen, legt das Objekt an und ruft geto() auf, um es zu fül-len? Dazu dient eine Funktion retrieve(), die in der KlassenbeschreibungsdateiObject.d vereinbart wird, die aber keine Methode ist:

void * retrieve (FILE * fp); // Objekt aus Dateiretrieve() liest einen Klassennamen als Zeichenkette aus einem Strom, findet ir-gendwie den Zeiger auf die richtige Klassenbeschreibung, beschafft Platz für dasObjekt mit allocate() und ruft geto() auf, um das Objekt zu füllen. Da allocate()schon die endgültige Klassenbeschreibung einträgt, kann tatsächlich geto() selbstauf die angelegte Fläche angewendet werden:

struct classList { const char * name; const void * class; };void * retrieve (FILE * fp){ char buf [BUFSIZ];

static struct classList * cL; // lokale Kopiestatic int cD = -1; // Anzahl Klassenif (cD < 0)

... build classList in cL[0..cD-1] ...if (! cD)

fputs("no classes known\n", stderr);else if (fp && ! feof(fp) && fscanf(fp, "%s", buf) == 1){ struct classList key, * p;

key.name = buf;if (p = bsearch(& key, cL, cD, sizeof key,

(int (*) (const void *, const void *)) cmp))return geto(allocate(p -> class), fp);

fprintf(stderr, "%s: cannot retrieve\n", buf);}return 0;

}

Page 175: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

165___________________________________________________________________________12.5 Objekte verbinden — ‘‘value’’ nochmals betrachtet

retrieve() benötigt eine Liste von Klassennamen und Zeigern auf die Beschreibun-gen. Die Klassenbeschreibungen zeigen auf die Methoden und Selektoren, dasheißt, die Liste garantiert sogar, daß die Implementierungen der Klassen mit demProgramm gebunden sind, das retrieve() verwendet. Wenn die Daten für ein Ob-jekt eingelesen werden, sind die Methoden für das Objekt im Programm vorhanden— geto() ist nur eine davon.

Woher kommt die Klassenliste? Wir könnten zwar eine von Hand anlegen, aberwir haben im neunten Kapitel munch kennengelernt, ein einfaches awk -Programm,das Klassennamen aus einer Liste von Objektmodulen extrahieren kann, die nm pro-duziert. Da nm normalerweise auch auf eine Bibliothek von Objektmodulen ange-wendet werden kann, können wir sogar die Klassenliste aus einer ganzen Bibliothekextrahieren. Das Resultat ist ein Vektor classes[] mit einer Liste von Zeigern auf dieInitialisierungsfunktionen der Klassen, alphabetisch nach Klassennamen sortiert.

Wir könnten in retrieve() diese Liste durchgehen, jede Funktion aufrufen, umdie initialisierte Klassenbeschreibung zu erhalten, und darauf dann nameOf() an-wenden, um den Klassennamen als Zeichenkette zu bekommen. Das ist nicht be-sonders effizient, wenn wir viele Objekte laden müssen. retrieve() konstruiert des-halb eine private Liste folgendermaßen:

extern const void * (* classes[]) (void); // munchif (cD < 0){ for (cD = 0; classes[cD]; ++ cD)

; // Klassen zaehlenif (cD > 0) // name/desc sammeln{ cL = malloc(cD * sizeof(struct classList));

assert(cL);for (cD = 0; classes[cD]; ++ cD)

cL[cD].class = classes[cD](),cL[cD].name = nameOf(cL[cD].class);

}}

Die private Liste hat zusätzlich den Vorteil, daß sie weitere Aufrufe der Initialisie-rungsfunktionen vermeidet.

12.5 Objekte verbinden — value nochmals betrachtetEine streng baumstrukturierte Menge von Objekten können wir mit puto() spei-chern und mit retrieve() und passenden geto-Methoden auch wieder laden. Unse-re Dialogsprache demonstriert, daß es ein Problem gibt, wenn eine Sammlung vonObjekten gespeichert wird, die auf andere Objekte verweisen, die in einem anderenZusammenhang — oder auch gar nicht — gespeichert werden. Zum Beispiel:

$ valuedef sqr = $ * $def one = sqr(sin($)) + sqr(cos($))save one

Page 176: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

166___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

Die Ausgabedatei one.sym enthält Verweise auf sqr, aber keine Definition:

$ cat one.symFun at 0x50ec9f8

name onelex 102value 10

=Add at 0x50ed168User at 0x50ed074 Ref

name sqr putsymbollex 102

Builtin at 0x50ecfd0 Refname sin putsymbollex 109

Parm at 0x50ecea8 Valname one putsymbollex 102

User at 0x50ed14cname sqrlex 102

Builtin at 0x50ed130name coslex 109

Parm at 0x50ed118name onelex 102

User ist eine Unterklasse von Ref, und Ref_puto() hat putsymbol() in parse.c ver-wendet, um nur den Symbolnamen und den Wert für den Erkenner auszugeben.Die Definition von sqr() wurde absichtlich nicht in one.sym gespeichert.

Wenn ein Verweis auf die Symboltabelle eingelesen wird, muß er wieder mitder Symboltabelle verbunden werden. Unsere Dialogsprache enthält eine einzigeSymboltabelle table, die in parse.c erzeugt und verwaltet wird, das heißt, ein Ver-weis aus einem Ausdrucksbaum in die Symboltabelle muß getsymbol() aus parse.cverwenden, um den Verweis mit der aktuellen Symboltabelle zu verbinden. JedeArt von Verweis verwendet eine andere Unterklasse von Node, damit getsymbol()ein Symbol in der richtigen Unterklasse von Symbol finden oder erzeugen kann.Deshalb haben wir Global-Knoten als Verweise auf Var-Symbole und Parm-Knotenals Verweise auf Fun-Symbole unterschieden.

% Global geto {struct Global * self = super_geto(Global(), _self, fp);down(self) = getsymbol(Var(), fp);return self;

}% Parm geto {

struct Parm * self = super_geto(Parm(), _self, fp);down(self) = getsymbol(Fun(), fp);return self;

}

Page 177: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

167___________________________________________________________________________12.5 Objekte verbinden — ‘‘value’’ nochmals betrachtet

Analog sucht ein Assign-Knoten nach einem Var-Symbol, Builtin nach Math undUser nach Fun. Sie alle verwenden getsymbol(), um ein geeignetes Symbol intable zu finden, zu erzeugen oder um sich zu beschweren, daß es zwar ein Symbolmit dem richtigen Namen, aber der falschen Klasse gibt:

void * getsymbol (const void * class, FILE * fp){ char buf [BUFSIZ];

int token;void * result;if (fscanf(fp, "\tname %s\n\tlex %d\n", buf, & token) != 2)

assert(0);result = screen(table, buf, UNDEF);if (lex(result) == UNDEF)

install(table, result =new(class, name(result), token));

else if (lex(result) != token){ fclose(fp);

error("%s: need a %s, got a %s",buf, nameOf(class), nameOf(classOf(result)));

}return result;

}Es ist ganz praktisch, daß bei der Erzeugung eines Fun-Symbols der zugehörigeAusdruck noch nicht eingetragen wird:

$ valueload oneone(10)undefined function

one() versucht, sqr() aufzurufen, aber diese Funktion ist noch nicht definiert.

let sqr = 9bad assignment

Ein undefiniertes Symbol wäre hier ersetzt worden, und die Zuweisung hätte funk-tioniert, das heißt, sqr() ist wirklich eine undefinierte Funktion.

def sqr = $ * $one(10)

1def sqr = 1one(10)

2Hier ist die Klassenhierarchie der Dialogsprache mit den meisten Methodendefinitio-nen. Die Metaklassen sind nicht dargestellt, Fettdruck zeigt, wo eine Methode zu-erst eingeführt wird:

Page 178: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

168___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

KLASSE DATEN METHODEN

Object magic, ... % classOf, ...%- delete, puto, geto, ...%+ new

Node % sunder%- delete, exec%+ new, reclaim

Number value %- ctor, puto, geto, execMonad down %- ctor

Val %- puto, execGlobal %- getoParm %- geto

Unary %- dtor, puto, getoMinus %- exec

Dyad left, right %- ctorRef %- dtor, puto

Assign %- geto, execBuiltin %- geto, execUser %- geto, exec

Binary %- dtor, puto, getoAdd %- execSub %- execMult %- execDiv %- exec

Symbol name, lex % name, lex%- ctor, puto, geto

Reserved %- deleteVar value % value, setvalue

%- puto, geto, moveConst %- ctor, delete, moveFun fun % setfun, funvalue

%- puto, geto, moveMath fun % mathvalue

%- ctor, deleteSymtab buf, ... % save, load, ...

%- ctor, puto, deleteEs bleibt ein leichte Ungereimtheit, mit der wir uns noch im nächsten Kapitel be-schäftigen: getsymbol() weiß anscheinend gut genug Bescheid, um den Dateizu-griff über fp abzuschließen, bevor error() aufgerufen wird, damit die Hauptschleifeweitergeht.

12.6 ZusammenfassungObjekte werden persistent genannt, wenn sie in Dateien gespeichert werden, damitsie die gleiche oder eine andere Anwendung später wieder laden kann. Persistente

Page 179: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

169___________________________________________________________________________12.6 Zusammenfassung

Objekte werden entweder durch explizite Aktionen oder implizit im Destruktor ge-speichert. Laden erfolgt an Stelle von Anlegen und Konstruieren von Objekten.

Zur Implementierung von persistenten Objekten benötigen wir zwei dynamischgebundene Methoden und eine Funktion, die den Ladevorgang steuert:

int puto (const _self, FILE * fp);void * geto (_self, FILE * fp);void * retrieve (FILE * fp);

puto() muß für jede Klasse von persistenten Objekten implementiert werden. DieMethode ruft sich zuerst selbst entlang der Oberklassen-Kette auf und schreibtdann die Instanzen-Variablen der eigenen Klasse in den Strom. Damit wird alle In-formation über ein Objekt in den Ausgabestrom geschrieben, beginnend mit derWurzelklasse.

geto() wird ebenfalls für alle persistenten Objekte implementiert. Die Methodeist normalerweise symmetrisch zu puto(), das heißt, nach dem Aufruf der Oberklas-sen-Kette füllt die Methode das Objekt mit den Werten für die Instanzen-Variablender eigenen Klasse, die puto() ausgegeben hat. geto() funktioniert wie ein Kon-struktor, das heißt, die Methode legt ihr Objekt nicht an, sondern trägt nur Informa-tion ein.

Die Ausgabe, die puto() produziert, beginnt mit dem Klassennamen des Ob-jekts. retrieve() liest den Klassennamen, findet die zugehörige Klassenbeschrei-bung, legt Speicher für das Objekt an und ruft geto() auf, um das Objekt zu füllen.Folglich schreibt zwar die Methode puto() der Wurzelklasse den Klassennamen je-des Objekts, aber die Methode geto() der Wurzelklasse beginnt, erst nach demKlassennamen zu lesen. retrieve() kann nur Objekte laden, für die Klassenbeschrei-bungen vorliegen, das heißt, bei ANSI-C müssen Methoden für persistente Objektea priori in einem Programm vorhanden sein, das die Objekte mit retrieve() ladenwill.

Abgesehen von einem Klassennamen am Anfang ist das Ausgabeformat fürputo() frei wählbar. Wenn jedoch die Ausgabe einfacher Text ist, kann man puto()auch zur Fehlersuche verwenden, denn die Methode kann dann von einem geeigne-ten Debugger aus auf beliebige Objekte angewendet werden.

Für einfache Objekte gibt man am besten die Werte aller Instanzen-Variablenaus. Für Container-Objekte, die auf andere Objekte zeigen, kann man puto() ver-wenden, um die Klientenobjekte auszugeben. Wenn jedoch ein Objekt in mehr alseinem anderen Objekt vorkommen kann, müssen puto() oder retrieve() sehr sorg-fältig entworfen werden, um den Effekt einer ‘‘tiefen’’, vollständigen Kopie zu ver-meiden, das heißt, puto() oder retrieve() müssen sicherstellen, daß die Klientenob-jekte eindeutig identifiziert werden. Eine recht robuste Lösung, um Objekte zu la-den, die eine einzige Applikation produziert hat, besteht darin, daß retrieve() eineTabelle der ursprünglichen Adressen aller geladenen Objekte konstruiert und ein Ob-jekt nur erzeugt, wenn es noch nicht in der Tabelle steht.

Page 180: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

170___________________________________________________________________________12 Persistente Objekte — Datenstrukturen speichern und laden

12.7 ÜberlegungenWenn retrieve() die ursprünglichen Adressen aller Objekte aufzeichnet und nurneue Objekte konstruiert, müssen wir in einem Strom ein Objekt überlesen.

System V enthält die Funktionen dlopen(), dlsym() und dlclose(), um soge-nannte shared objects, dynamisch zu laden. retrieve() könnte damit die Implemen-tierung einer Klasse nach Namen laden. Der Klassenmodul enthält die Klassenbe-schreibung samt allen Methoden. Es ist allerdings nicht klar, wie wir effizient aufdie neu geladenen Selektoren zugreifen würden.

value kann um Kontrollstrukturen erweitert werden, so daß Funktionen wesent-lich mächtiger werden. In diesem Fall muß man stmt() in echte Anweisungen wieKontrollstrukturen oder let und in Kommandos wie save, load oder def aufteilen.

Page 181: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

171___________________________________________________________________________

13Exceptions

Fehlerbehandlung mit System

Dreizehn ist die richtige Nummer für ein Kapitel über Fehler. Wenn wir in einer ein-zigen Funktion verlorengehen, können wir durchaus mit dem oft verpönten gotoaussteigen. ANSI-C’s setjmp() und longjmp() räumen ein Nest von verschachteltenFunktionsaufrufen weg, wenn wir tief drinnen ein Problem entdecken. Wenn wir al-lerdings auf verschiedenen Ebenen einer Rettungsaktion Aufräumarbeiten durch-führen müssen, sollten wir das brutale Durchgreifen von setjmp() ein bißchen zü-geln.

13.1 StrategieWenn unsere Dialogsprache Probleme hat, eine Funktionsdefinition zu laden, sehenwir ein typisches Fehlerbehandlungsproblem: Ein offener Eingabestrom muß ge-schlossen werden, bevor wir error() aufrufen können, um eine Fehlermeldung aus-zugeben und zur Hauptschleife zurückzukehren. Das folgende Bild deutet an, daßeine einfache riskante Aktion in Logik zur Fehlerbehandlung verpackt werden sollte:

riskante Aktion

on error

error handler

Zuerst wird die Fehlerbearbeitung vorbereitet. Entweder geht die riskante Aktionkorrekt zu Ende, oder die Fehlerbehandlung darf aufräumen, bevor die Aktion insge-samt beendet wird. In ANSI-C verwenden wir setjmp() und longjmp(), um diesesSchema zur Fehlerbehandlung zu implementieren:

#include <setjmp.h>static jmp_buf onError;static void cause() {

longjmp(onError, 1);}action () {

if (! setjmp(onError))riskante Aktion

elseFehlerbehandlung

}setjmp() initialisiert onError und liefert Null. Wenn in der riskanten Aktion oder ineiner von dort aufgerufenen Funktion etwas schiefgeht, signalisieren wir den Fehler,indem wir cause() aufrufen. Der Aufruf von longjmp() in dieser Funktion benützt

Page 182: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

172___________________________________________________________________________13 Exceptions — Fehlerbehandlung mit System

die Information in onError und läßt den Aufruf von setjmp(), der onError initialisierthat, nochmals zu Ende gehen. Der zweite Rücksprung von setjmp() liefert daszweite Argument von longjmp() als Funktionswert; wenn dieser Wert null ist, istdas Resultat eins. Wenn die Funktion, die setjmp() aufgerufen hat, nicht mehr akti-viert ist, geht das Ganze schrecklich schief.

In der Terminologie der obigen Abbildung bezieht sich on error auf den Aufrufvon setjmp(), der die Information zur Fehlerbehandlung hinterlegt. Die riskanteAktion wird aufgerufen, wenn setjmp() Null als Resultat liefert; andernfalls wird derCode für error handler ausgeführt. cause() muß aufgerufen werden, um die Fehler-behandlung einzuleiten.

Wir haben dieses einfache Model im Abschnitt 3.1 zur Behandlung von Syntax-fehlern in unserer Dialogsprache verwendet. Dort sind wir in der Regel tief in rekur-sive Funktionsaufrufe verstrickt, die durch longjmp() abgeräumt werden. Es wirdein bißchen komplizierter, wenn Fehlerbehandlungen verschachtelt werden müs-sen. Hier ist, was bei einer load-Operation in der Dialogsprache passiert:

load file

on load error

close file

on error

message

Hauptschleife

Hier brauchen wir zwei Puffer, aus denen longjmp() die nötigen Informationen ho-len kann: onError kehrt zur Hauptschleife zurück, und onLoadError dient zum Auf-räumen nach einer fehlerhaften load-Operation:

jmp_buf onError, onLoadError;#define cause(x) longjmp(x, 1)mainLoop () {

if (! setjmp(onError))loadFile();

elseein Problem

}loadFile () {

if (! setjmp(onLoadError))Datei bearbeiten

elseDateizugriff beendencause(onError);

}

Page 183: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

173___________________________________________________________________________13.2 Implementierung — ‘‘Exception’’

Die Skizze zeigt, daß cause() irgendwie wissen muß, wie weit zurück die Fehlerbe-handlung gehen soll. Wir können ein Argument oder eine verborgene globale Struk-tur für diesen Zweck verwenden.

Wenn wir cause() ein explizites Argument geben, wird das wahrscheinlich einglobales Symbol sein, damit es auch aus anderen Dateien heraus benutzt werdenkann. Offensichtlich darf ein derartiges globales Symbol nicht zum falschen Zeit-punkt verwendet werden. Es hat außerdem den Nachteil, daß es Teil der Anwen-dung ist, das heißt, obgleich das Symbol eigentlich nur zu einer bestimmten Fehler-behandlung selbst gehört, muß es in den Code geschrieben werden, den diese Feh-lerbehandlung schützen soll, also auch zum Beispiel in Funktionen, die dieser Codeaufruft. Wenn wir diese Funktionen an einer anderen Stelle benutzen, können wirsehr leicht versehentlich versuchen, zu einer inaktiven Fehlerbehandlung zu sprin-gen.

Eine wesentlich bessere Strategie ist ein Stack von jmp_buf-Werten. In einerFunktion richten wir eine Fehlerbehandlung ein, indem wir einen Wert auf diesemStack ablegen, und cause() verwendet jeweils den obersten Eintrag. Die Fehlerbe-handlung muß natürlich von dem Stack entfernt werden, bevor die zugehörige Funk-tion beendet wird.

13.2 Implementierung — ExceptionException ist eine Klasse für verschachtelte Behandlung von Fehlerbedingungen.Exception-Objekte müssen in umgekehrter Reihenfolge ihrer Erzeugung zerstörtwerden. Normalerweise repräsentiert das neuste Objekt die Fehlerbehandlung, diedurch einen Aufruf von cause() ausgelöst wird.

// new(Exception())#include <setjmp.h>void cause (int number); // armed? goto catch()% Class Exception: Object {

int armed; // != 0 nach catch()jmp_buf label; // fuer catch()

%void * catchException (_self);

%}new(Exception()) erzeugt ein Exception-Objekt, das auf einem verborgenen Stackaller derartigen Objekte abgelegt wird.

#include "List.h"static void * stack;% Exception ctor {

void * self = super_ctor(Exception(), _self, app);if (! stack)

stack = new(List(), 10);addLast(stack, self);return self;

}

Page 184: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

174___________________________________________________________________________13 Exceptions — Fehlerbehandlung mit System

Wir benützen ein List-Objekt aus Abschnitt 7.7, um den Stack für Exception-Objek-te zu implementieren.

Exception-Objekte müssen genau in umgekehrter Reihenfolge ihrer Erzeugungfreigegeben werden. Der Destruktor entfernt sie vom Stack:

% Exception dtor {void * top;

%castsassert(stack);top = takeLast(stack);assert(top == self);return super_dtor(Exception(), self);

}Eine Fehlerbehandlung wird ausgelöst, wenn man cause() mit einem von Null ver-schiedenen Argument, der Fehlernummer, aufruft. Wenn möglich sorgt cause() füreinen longjmp() zum obersten Exception-Objekt auf dem Stack, das heißt, zum zu-letzt erzeugten derartigen Objekt. cause() kann — muß aber nicht — zu seinem ei-genen Aufrufer zurückkehren.

void cause (int number) {unsigned cnt;if (number && stack && (cnt = count(stack))){ void * top = lookAt(stack, cnt-1);

struct Exception * e = cast(Exception(), top);if (e -> armed)

longjmp(e -> label, number);}

}cause() ist eine Funktion, keine Methode. Die Funktion wird aber trotzdem als Teilder Implementierung der Klasse Exception realisiert und hat definitiv Zugriff auf dieinternen Daten dieser Klasse. Eine derartige Funktion wird oft als friend der Klassebezeichnet.

cause() prüft eine Reihe von Bedingungen: Das Argument darf nicht null sein,der Stack von Exception-Objekten muß existieren und Objekte enthalten, das ober-ste Objekt muß zur Klasse Exception gehören, und vor allem muß dieses Objektauf den Empfang des Fehlers vorbereitet worden sein. Wenn eine der Bedingun-gen nicht erfüllt ist, geht der Aufruf von cause() zu Ende, und der Aufrufer mußselbst mit der Situation fertigwerden.

Ein Exception-Objekt muß auf den Empfang eines Fehlers vorbereitet sein, dasheißt, die jmp_buf-Information muß mit setjmp() hinterlegt werden, bevor cause()das Objekt verwendet. Aus mehreren Gründen sind Erzeugen und Vorbereiten desObjekts zwei getrennte Operationen. Ein Objekt wird normalerweise mit new() er-zeugt, und das Objekt ist das Resultat dieser Operation. Ein Exception-Objekt mußmit setjmp() auf seinen Einsatz vorbereitet werden, und diese Funktion liefert zwei-mal ein ganzzahliges Resultat: zuerst null und beim zweiten Mal den Wert, der an

Page 185: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

175___________________________________________________________________________13.3 Beispiele

longjmp() übergeben wurde. Es ist schwer vorstellbar, wie wir die beiden Opera-tionen kombinieren könnten.

Wichtiger ist noch, daß ANSI-C stark einschränkt, wo setjmp() überhaupt aufge-rufen werden darf. Der Funktionsaufruf muß so ziemlich allein in einem Ausdruckstehen, und der Ausdruck kann nur entweder eine Anweisung sein oder eine Schlei-fe oder Auswahl-Anweisung, wie if oder switch, kontrollieren. Eine elegante Lö-sung zur Vorbereitung eines Exception-Objekts ist folgender Makro, der inException.d definiert wird:

#define catch(e) setjmp(catchException(e))catch() wird dort benützt, wo normalerweise setjmp() angegeben würde, dasheißt, die Einschränkungen, die ANSI-C für setjmp() trifft, müssen für catch() be-achtet werden. Dieser Makro liefert später den Wert, der an cause() übergebenwurde. Der Trick liegt im Aufruf der Methode catchException(), um das Argumentfür setjmp() zu liefern:

% catchException {%casts

self -> armed = 1;return self -> label;

}catchException() setzt die Bedingung .armed und liefert jmp_buf, damit setjmp()diesen Puffer initialisieren kann. Nach dem ANSI-C Standard ist jmp_buf ein Vektor-typ, das heißt, der Name einer jmp_buf-Variablen repräsentiert ihre Adresse. Fallsein C-System jmp_buf versehentlich als Struktur definiert hat, müßten wir nur dieAdresse explizit liefern. Es ist übrigens nicht nötig, daß catch() unbedingt auf dasoberste Element im Stack angewendet wird.

13.3 BeispieleIn unserer Dialogsprache können wir den expliziten Aufruf von setjmp() in parse.cdurch ein Exception-Objekt ersetzen:

int main (void){ volatile int errors = 0;

char buf [BUFSIZ];void * retry = new(Exception());...if (catch(retry)){ ++ errors;

reclaim(Node(), delete);}while (gets(buf))

...Wenn wir jetzt eine Fehlerbedingung verursachen, kommen wir zur Hauptschleifezurück. Am Schluß von error() wird cause() aufgerufen, um die Fehlerbehandlungauszulösen:

Page 186: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

176___________________________________________________________________________13 Exceptions — Fehlerbehandlung mit System

void error (const char * fmt, ...){ va_list ap;

va_start(ap, fmt);vfprintf(stderr, fmt, ap), putc(’\n’, stderr);va_end(ap);cause(1);assert(0);

}error() wird bei jedem Fehler aufgerufen, der in der Dialogsprache entdeckt wird.Die Funktion gibt eine Fehlermeldung aus und ruft cause() auf, was normalerweisedirekt zurück zur Hauptschleife führt. Wenn Symtab aber seine Methode load aus-führt, wird dafür eine eigene Fehlerbehandlung hinterlegt:

% Symtab load {FILE * fp;void * in;void * cleanup;...if (! (fp = fopen(fnm, "r")))

return EOF;cleanup = new(Exception());if (catch(cleanup)){ fclose(fp);

delete(cleanup);cause(1);assert(0);

}while (in = retrieve(fp))

...fclose(fp);delete(cleanup);return result;

}Wir sahen im Abschnitt 12.5, daß es schwierig wird, wenn load() einen Ausdruckbearbeitet und getsymbol() einen Namen nicht mit dem zugehörigen Symbol intable verbinden kann:

else if (lex(result) != token)error("%s: need a %s, got a %s",

buf, nameOf(class), nameOf(classOf(result)));Jetzt müssen wir nur noch error() aufrufen, um eine Fehlermeldung auszugeben.error() löst eine Fehlerbedingung aus, die in diesem Fall durch das Exception-Objekt cleanup in load() behandelt wird. Bei dieser Fehlerbearbeitung ist bekannt,daß der Strom fp aufgelöst werden muß, bevor load() abgebrochen werden kann.Wenn das Exception-Objekt cleanup zerstört und eine weitere Fehlerbedingungausgelöst wird, findet schließlich wieder die normale Fehlerbehandlung bei retry inmain() statt, wo verlorene Knoten freigegeben werden und die Hauptschleife wie-der beginnt.

Page 187: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

177___________________________________________________________________________13.3 Beispiele

Das Beispiel zeigt, daß wir cause() am besten als Funktion entwerfen, die nureine Fehlernummer weiterleitet. error() kann dann aus verschieden geschützten Si-tuationen heraus aufgerufen werden und führt automatisch zur richtigen Fehlerbe-handlung. Wenn wir dort das zugehörige Exception-Objekt freigeben und die Feh-lerbedingung mit cause() nochmals auslösen, erreichen wir alle Fehlerbehandlungenentlang der Kette.

Fehlerbehandlung erinnert an goto mit allen seinen ungezügelten Eigenschaf-ten. Das folgende, grausame Beispiel produziert mit einer switch-Anweisung undzwei Exception-Objekten folgende Ausgabe:

$ exceptcaused -1caused 1caused 2caused 3caused 4

Hier ist der Code; es gibt Extrapunkte, wenn man den Ablauf mit einem Bleistiftnachzeichnet...

int main (){ void * a = new(Exception()), * b = new(Exception());

cause(-1); puts("caused -1");switch (catch(a)) {case 0:

switch (catch(b)) {case 0:

cause(1); assert(0);case 1:

puts("caused 1");cause(2); assert(0);

case 2:puts("caused 2");delete(b);cause(3); assert(0);

default:assert(0);

}case 3:

puts("caused 3");delete(a);cause(4);break;

default:assert(0);

}puts("caused 4");return 0;

}

Page 188: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

178___________________________________________________________________________13 Exceptions — Fehlerbehandlung mit System

Der Code ist definitiv schrecklich und unverständlich. Wenn wir jedoch dieException-Objekte dazu verwenden, nur das Paket zu konstruieren, das am Anfangdieses Kapitels gezeigt wurde

riskante Aktion

on error

error handler

dann codieren wir immer noch im Stil der Kontrollstrukturen mit einem Eingang undeinem Ausgang, die die Grundlage der Strukturierten Programmierung bilden. DieException-Klasse ist ein sauberer, verkapselter Mechanismus, um Fehlerbehand-lungen und damit eine geschützte, riskante Aktion in eine andere zu schachteln.

13.4 ZusammenfassungModerne Programmiersprachen wie Eiffel oder C++ verfügen über eine besondereSyntax zur Fehlerbehandlung. Bevor eine riskante Aktion versucht wird, führt manzuerst eine Fehlerbehandlungsroutine ein. Während der riskanten Aktion könnenSoftware oder Hardware (Signale und Unterbrechungen) einen Fehler auslösen unddamit die Fehlerbehandlung zur Ausführung bringen. Theoretisch gibt es danachdrei Möglichkeiten: Terminieren, und zwar beides, die Fehlerbehandlung und dieriskante Aktion; Fortsetzen der riskanten Aktion unmittelbar nach dem Punkt, woder Fehler entstand und die Fehlerbehandlung ausgelöst wurde; oder Wiederholendes Teils der riskanten Aktion, der den Fehler verursacht hat.

In der Praxis wird man meistens terminieren, und das kann durchaus die einzigeMöglichkeit sein, die die Programmiersprache bietet. Eine Programmiersprachesollte aber auf alle Fälle das Verschachteln von Fehlerbehandlungen erlauben, undman muß die Fehlerbehandlungen verketten können, das heißt, wenn eine Fehler-behandlung erfolgt ist, muß es möglich sein, die nächste Fehlerbehandlung weiterrückwärts auszulösen.

Fehlerbehandlung mit Terminierung kann in ANSI-C sehr leicht mit setjmp() im-plementiert werden. Fehlerbehandlungen kann man schachteln, indem man diejmp_buf-Information, die setjmp() anlegt und longjmp() benützt, auf einem Stackablegt. Einen Stack von solchen jmp_buf-Werten kann man als Objekte einer Klas-se Exception verwalten. Ein Exception-Objekt muß mit dem Makro catch() vorbe-reitet werden und liefert dann eine von Null verschiedene Fehlernummer als zwei-tes Resultat von catch(). Eine Fehlerbehandlung wird mit der Funktion cause() aus-gelöst. Dabei übergibt man den Fehlercode, den catch() für das neuste Objekt alszweites Resultat liefern soll.

13.5 ÜberlegungenWahrscheinlich kann man relativ leicht vergessen, einige verschachtelte Exception-Objekte freizugeben. Exception_dtor() könnte implizit so lange Objekte vom Stack

Page 189: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

179___________________________________________________________________________13.5 Überlegungen

entfernen, bis self gefunden wird. Ist es eine gute Idee, diese Objekte mit delete()freizugeben, um Speicherlecks zu vermeiden? Was passiert, wenn self im Stacknicht vorhanden ist?

Analog könnte cause() auf dem Stack nach dem nächstgelegenen, vorbereite-ten Exception-Objekt suchen. Sollte man die unvorbereiteten Objekte entfernen?

setjmp() ist gefährlich, denn die Funktion schützt nicht vor einem Rücksprungin eine Funktion, die selbst bereits nicht mehr aktiv ist. Normalerweise verwendetANSI-C einen Stack von sogenannten activation records, um lokale Information fürjeden aktiven Funktionsaufruf abzulegen. cause() muß natürlich auf einer höherenEbene auf diesem Stack aufgerufen werden als die Funktion, zu der longjmp()zurückspringen soll. Wenn die Methode catchException() die Adresse einer loka-len Variablen ihres Aufrufers übergibt, könnten wir sie mit der jmp_buf-Informationspeichern und zu einer groben Plausibilitätsprüfung des Aufrufs von longjmp() ver-wenden. Als raffiniertere Technik könnten wir eine magische Zahl in der lokalen Va-riablen speichern und nachsehen, ob sie noch immer vorhanden ist. In einer nicht-portablen Lösung können wir vielleicht eine Zeigerkette auf dem Stack der activa-tion records verfolgen und von cause() aus untersuchen, ob der activation recorddes Aufrufers von catchException() noch vorhanden ist.

Page 190: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 191: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

181___________________________________________________________________________

14Nachrichten weiterleiten

Ein GUI-Rechner

In diesem Kapitel betrachten wir ein ziemlich typisches Problem: Eine Klassenhier-archie entwickeln wir selbst, um eine Anwendung zu unterstützen, und eine andereKlassenhierarchie wird uns mehr oder weniger aufgezwungen, denn sie unterstütztSystemfunktionen wie zum Beispiel eine grafische Schnittstelle.* An diesem Punktverwenden richtige Programmierer mehrfache Vererbung, aber, wie unser Pflicht-beispiel eines primitiven Taschenrechners demonstriert, man kann eine elegante Lö-sung mit einem Bruchteil dieses Aufwands realisieren.

14.1 Die Idee — forwardJede dynamisch gebundene Methode wird über ihren Selektor aufgerufen, und wirhaben im achten Kapitel gesehen, daß der Selektor untersucht, ob seine Methodefür das angegebene Objekt gefunden werden kann. Betrachten wir als Beispiel denSelektor add() für die Methode, mit der ein Objekt zu einer List hinzugefügt wird:

struct Object * add (void * _self, const void * element) {struct Object * result;const struct ListClass * class =

cast(ListClass(), classOf(_self));

assert(class -> add.method);cast(Object(), element);

result = ((struct Object * (*) ())class -> add.method)(_self, element);

return result;}

classOf() untersucht, ob _self auf ein Objekt verweist. Der umgebende Aufruf voncast() stellt sicher, daß die Klassenbeschreibung von _self zur MetaklasseListClass gehört, das heißt, daß sie wirklich einen Zeiger auf eine add-Methodeenthält. Zum Schluß schützt uns assert() davor, daß sich ein Nullzeiger ein-schleicht, das heißt, assert() kontrolliert, daß eine add-Methode irgendwo aufwärtsauf der Vererbungskette implementiert wurde.

Was passiert, wenn der Selektor add() auf ein Objekt angewendet wird, das nievon dieser Methode gehört hat, das heißt, was passiert, wenn _self die verschiede-nen Tests im Selektor add() nicht besteht? Bei der bisherigen Implementierungwird irgendwo assert() ausgelöst, der Fehler ist eingegrenzt, und unser Programmbricht ab.

Angenommen, wir arbeiten mit einer Klasse X von Objekten, die selbst keineAbkömmlinge von List sind, die aber ein List-Objekt kennen, an das sie aus logi-____________________________________________________________________________________________

* Graphical user interface, kurz GUI — es ist sicher kein Zufall, daß das wie gooey ausgesprochen wird.Letzteres bedeutet klebrig und bezieht sich unbedingt auf die charmanten Eigenschaften derartigerSchnittstellen...

Page 192: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

182___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

scher Sicht einen Aufruf von add() weiterleiten könnten. So wie es jetzt ist, müßteder Benutzer von X-Objekten wissen (oder mit respondsTo() herausfinden), daßadd() nicht auf sie angewendet werden kann und den Aufruf eben selbst umlenken.Betrachten wir aber einmal den folgenden, leicht modifizierten Selektor:

struct Object * add (void * _self, const void * element) {struct Object * result;const struct ListClass * class =

(const void *) classOf(_self);

if (isOf(class, ListClass()) && class -> add.method) {cast(Object(), element);result = ((struct Object * (*) ())

class -> add.method)(_self, element);} else

forward(_self, & result, (Method) add, "add",_self, element);

return result;}

Jetzt kann sich _self auf ein beliebiges Objekt beziehen. Wenn seine Klasse zufälligeinen verwendbaren add-Zeiger besitzt, wird die Methode so wie vorher aufgeru-fen. Andernfalls wird die gesamte Information einer neuen Methode forward()übergeben: das Objekt selbst, eine Fläche für das erwartete Resultat, einen Zeigerauf den Selektor, der nicht funktioniert hat, sowie sein Name und schließlich dieWerte der ursprünglichen Argumentliste. forward() selbst ist eine dynamisch ge-bundene Methode, die in der Klasse Object vereinbart wird:

% Class Object {...

%-void forward (const _self, void * result, \

Method selector, const char * name, ...);

Offensichtlich ist die ursprüngliche Definition ein bißchen hilflos:% Object forward {%casts

fprintf(stderr, "%s at %p does not answer %s\n",nameOf(classOf(self)), self, name);

assert(0);}

Wenn ein Object selbst eine Methode nicht versteht, haben wir einfach keine Wahl.forward() ist aber dynamisch gebunden: Wenn eine Klasse Nachrichten weiterlei-ten will, kann sie das dadurch erreichen, daß sie forward() ersetzt. Wir werden ineinem Beispiel im Abschnitt 14.6 sehen, daß diese Möglichkeit fast so gut ist, wiewenn ein Objekt gleichzeitig zu mehreren Klassen gehört.

14.2 ImplementierungGlücklicherweise haben wir im siebten Kapitel beschlossen, daß wir unseren Co-dierstandard mit einem Präprozessor ooc realisieren, und Selektoren gehören zu

Page 193: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

183___________________________________________________________________________14.2 Implementierung

diesem Standard. Im Abschnitt 8.4 haben wir uns den Report selector angesehen,der alle Selektoren generiert. Nachrichten werden weitergeleitet, indem wir for-ward(), so wie oben gezeigt, vereinbaren, eine triviale Methode implementierenund den Report selector in etc.rep so modifizieren, daß alle generierten Selektorendie Aufrufe umlenken, die sie nicht verstehen:*

`%header { `n`%result`%classOf

`%ifmethod`%checks`%call`t } else `n`%forward`%return} `n `n

Das ist fast der gleiche Code wie im Abschnitt 8.4: Wie wir vorher gesehen haben,wird der Aufruf von cast() im Report classOf in einen Aufruf von isOf() abgeändert— das ist ein Teil des Reports ifmethod — und ein else-Zweig wird mit dem Reportforward hinzugefügt, der den Aufruf von forward() durchführt.

Der Aufruf von forward() geht nochmals durch einen Selektor, damit die Argu-mente geprüft werden. Es ist sicher keine gute Idee, wenn wir hier in rekursiveAufrufe abgleiten, folglich brechen wir das Verfahren mit assert() ab, wenn wir denSelektor forward() selbst generieren:

% forward // Aufrufe (bis auf forward) weiterleiten

`{if `method forward`t `t assert(0);`} `{else`t `t forward(_self, \

`{if `result void 0, `} `{else & result, `} \(Method) `method , " `method ", `%args );

`} `n

Die zusätzliche `{if-Gruppe beschäftigt sich mit der Tatsache, daß ein Selektor letzt-lich das Resultat liefern muß, das sein Aufrufer erwartet. Das Resultat muß vonforward() produziert werden. Als allgemeine Lösung übergeben wir eine Resultat-fläche an forward(), damit sie irgendwie gefüllt wird. Wenn aber unser Selektormit void als Resultattyp vereinbart wurde, haben wir keine Variable result; dannübergeben wir eben einen Nullzeiger.

Es sieht so aus, als ob wir von Hand ein bißchen besser codieren könnten:Manchmal könnten wir ohne die Variable result, die Zuweisung und eine separatereturn-Anweisung auskommen. Diese Art von Kleinarbeit wäre zwar in den Re-ports prinzipiell möglich, aber sie macht die Reports unnötig kompliziert — jedervernünftige Compiler generiert ohnehin in jedem Fall den gleichen Maschinencode.

____________________________________________________________________________________________

* Wie früher ist der abgedruckte Report leicht vereinfacht. Die Teile, die sich mit variablen Argumentli-sten beschäftigen, wurden ausgelassen.

Page 194: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

184___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

classOf ist der andere Report, der ernsthaft verändert wird. Ein Aufruf voncast() wird entfernt, aber die spannende Frage ist, was passiert, wenn ein Aufruf ei-ner Klassenmethode weitergeleitet werden muß. Betrachten wir dazu den Selek-tor, den ooc für new() generiert:

struct Object * new (const void * _self, ...) {struct Object * result;va_list ap;const struct Class * class = cast(Class(), _self);

va_start(ap, _self);if (class -> new.method) {

result = ((struct Object * (*) ()) class -> new.method)(_self, & ap);

} elseforward((void *) _self, & result, (Method) new, "new",

_self, & ap);va_end(ap);return result;

}

new() wird für eine Klassenbeschreibung wie List aufgerufen. Eine Klassenmetho-de sollte man unbedingt nur für Klassenbeschreibungen aufrufen, deshalb wirdcast() verwendet, um dies zu erzwingen. Die Methode new gehört zu Class, des-halb muß isOf() nicht aufgerufen werden.

Nehmen wir einmal an, wir hätten die Methode Object_new() versehentlichnicht implementiert, das heißt, List hat dann keine Methode für new geerbt, undnew.method ist ein Nullzeiger. In diesem Fall wird dann forward() für List aufge-rufen. forward() ist aber eine dynamisch gebundene Methode, keine Klassenme-thode. Deshalb sucht der Selektor forward() in der Klassenbeschreibung von Listnach einer Methode forward, das heißt, der Selektor sucht nach etwas wieListClass_forward():

ListClass

Class

"ListClass"

Class

sizeof List

List füllen

unmöglich

nichts tun

List weiterleiten

List erzeugen

struct Class

ctor:

dtor:

delete:

forward:

new:

List

"List"

Object

sizeof aList

aList füllen

aList leeren

aList freigeben

aList weiterleiten

aList erzeugen

zu aList hinzufügen

aus aList entfernen

struct ListClass

ctor:

dtor:

delete:

forward:

new:

add:

take:

aList

...

struct List

Page 195: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

185___________________________________________________________________________14.3 Ein Beispiel für objekt-orientiertes Entwerfen

Das ist völlig akzeptabel: List_forward() ist für alle Nachrichten verantwortlich, dieaList nicht versteht, ListClass_forward() kümmert sich um alle, die List nicht ver-arbeiten kann. Hier ist der Report classOf in etc.rep:

`{if `linkage %-`{if `meta `metaroot`t const struct `meta * class = classOf(_self); `n`} `{else`t const struct `meta * class = ` \

(const void *) classOf(_self); `n`}`} `{else`t const struct `meta * class = ` \

cast( `metaroot (), _self); `n`} `n

Für dynamisch gebundene Methoden hat `linkage den Ersatztext %−. In diesemFall erhalten wir die Klassenbeschreibung als eine struct Class von classOf(), aberwir wandeln sie sofort abwärts in die Klassenbeschreibungsstruktur um, die siewirklich ist, wenn es isOf() gelingt, den Typ zu verifizieren. In der Unterklassen-Struktur können wir dann die richtige Methodenkomponente wählen.

Bei einer Klassenmethode hat `linkage den Ersatztext %+, das heißt, wir ver-wenden die zweite Hälfte des Reports und untersuchen einfach mit cast(), daß_self wenigstens zu Class gehört. Das ist aber auch schon der einzige Unterschiedin einem Selektor für eine Klassenmethode, der Nachrichten weiterleiten kann.

14.3 Ein Beispiel für objekt-orientiertes EntwerfenGrafische Benutzerschnittstellen müssen praktisch immer herhalten, um die unbe-grenzten Möglichkeiten von objekt-orientierten Techniken vorzuführen. Das Stan-dard-Beispiel ist meistens ein kleiner Taschenrechner, den man mit der Maus odervon der Tastatur aus bedienen kann:

Anzeige C

7 8 9 +

4 5 6 −

1 2 3 *

Q 0 = /

Wir konstruieren jetzt einen derartigen Taschenrechner für curses und X11 als Bild-schirm-Plattformen. Zuerst verwenden wir objekt-orientierte Techniken zum Ent-wurf und zur Implementierung einer allgemeinen Lösung. Wenn sie funktioniert,verbinden wir sie mit diesen zwei völlig inkompatiblen grafischen Umgebungen.Dabei werden wir schließlich sehen, wie elegant man mit forward() arbeiten kann.

Page 196: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

186___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Es hilft, wenn man den Algorithmus einer Anwendung funktionsfähig imple-mentiert hat, bevor man sich mit einer GUI-Bibliothek auseinandersetzt. Es hilftauch, wenn man die Aufgabe einer Anwendung als Objekte modelliert, die sichNachrichten übermitteln. Betrachten wir deshalb einfach einmal, welche Objektewir in dem Taschenrechner entdecken können, der oben abgebildet ist.

Der Taschenrechner hat Knöpfe, einen Computer-Chip und eine Anzeige. DieAnzeige empfängt Information und stellt sie dar. Der Computer-Chip ist ein Infor-mationsfilter: Er erhält Information, ändert seinen Zustand und schickt veränderteInformation weiter. Ein Knopf ist ein Informationssender oder auch -filter: Wenn ergenügend gereizt wird, schickt er Information ab.

Bisher haben wir mindestens vier Arten von Objekten entdeckt: die Anzeige,den Computer-Chip, die Knöpfe und die Information, die zwischen den Objektenfließt. Vielleicht gibt es eine fünfte Art von Objekt, nämlich eine Reizquelle fürKnöpfe, die unsere Tastatur, eine Maus etc. modelliert.

Einige dieser Klassen haben etwas gemeinsam: Eine Anzeige, ein Computer-Chip oder ein Knopf kann jeweils mit einem nachfolgenden Objekt verdrahtet wer-den, und die Information fließt diesen Draht entlang. Ein reiner Informations-empfänger, wie die Anzeige, empfängt zwar nur Information, ist nicht weiter ver-drahtet und gibt keine Information weiter, aber das stört das Prinzip nicht. Bishererhalten wir folgenden Entwurf:

KLASSE DATEN METHODEN

Object Wurzelklasse

Event Informationkind Art der Datendata Text, Position etc.

Ic Basisklasse der Anwendungout Objekt, mit dem ich verbunden bin

wire verbinde mich mit anderem Objektgate sende Information an out

LineOut modelliert die Anzeigewire unbenutztgate stelle empfangene Information dar

Button modelliert Eingabegerättext Etikett, interessante Information

gate betrachte empfangene Information:wenn wie text, weitersenden

Calc Computer-ChipZustand aktueller Wert etc.

gate ändere Zustand je nach Information,eventuell aktuellen Wert weitersenden

Page 197: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

187___________________________________________________________________________14.3 Ein Beispiel für objekt-orientiertes Entwerfen

Das sieht gut genug aus, daß wir die wesentlichen Methoden vereinbaren und einHauptprogramm schreiben können, um die Zerlegung unserer Problemwelt in Klas-sen zu testen.

Ic.denum react { reject, accept };

% IcClass: Class Ic: Object {void * out;

%-void wire (Object @ to, _self);enum react gate (_self, const void * item);

%}

% IcClass LineOut: Ic {%}

% IcClass Button: Ic {const char * text;

%}

run.cint main (){ void * calc = new(Calc());

void * lineOut = new(LineOut());void * mux = new(Mux());static const char * const cmd [] = { "C", "Q",

"0", "1", "2", "3", "4", "5", "6", "7", "8", "9","+", "-", "*", "/", "=", 0 };

const char * const * cpp;

wire(lineOut, calc);for (cpp = cmd; * cpp; ++ cpp){ void * button = new(Button(), * cpp);

wire(calc, button), wire(button, mux);}

Beinahe. Wir können den Computer-Chip, eine Anzeige und beliebig viele Knöpfeaufbauen und verbinden. Wenn wir das dann aber testen wollen, indem wir Zei-chen mit der Tastatur eingeben, müssen wir jedes Zeichen als Event-Objekt ver-packen und der Reihe nach jedem Button-Objekt anbieten, bis ein Knopf das Zei-chen akzeptiert und accept liefert, wenn wir seine Methode gate() aufrufen. Eineweitere Klasse wäre ganz praktisch:

KLASSE DATEN METHODEN

Object Wurzelklasse

Ic Basisklasse der Anwendung

Mux Multiplexer, eine Eingabe an viele Ausgängelist List mit Objekten

wire verbinde mich mit anderem Objektgate gebe Information weiter,

bis sie ein Objekt akzeptiert

Page 198: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

188___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Das oben gezeigte Hauptprogramm verwendet bereits ein Mux-Objekt und verbin-det es mit allen Button-Objekten. Alles ist jetzt vorbereitet für die Hauptschleife:

while ((ch = getchar()) != EOF)if (! isspace(ch)){ static char buf [2];

void * event;

buf[0] = ch;gate(mux, event = new(Event(), 0, buf));delete(event);

}return 0;

}

Zwischenraum wird ignoriert. Jedes andere Zeichen wird als Event-Objekt mit kindNull und dem Zeichen in einer Zeichenkette verpackt. Die Information wird demMultiplexer übergeben, und die Rechnerei nimmt ihren Lauf.

ZusammenfassungDieser Entwurf ging ungefähr so vor, wie das die Class-Responsibility-Collaborator-Technik empfiehlt, die zum Beispiel in [Bud91] beschrieben wird: Wir erkennen Ob-jekte, indem wir mehr oder weniger die Aufgabenstellung anstarren. Ein Objekt istverantwortlich dafür, bestimmte Dinge zu erledigen. Damit kommt man zu anderenObjekten, die helfen, die Arbeit zu erledigen. Wenn alle Objekte bekannt sind, stelltman sie in Klassen zusammen und findet hoffentlich eine hierarchische Anordnung,die nicht nur eine Ebene tief ist.

Der Schlüssel zu unserer Anwendung ist die Ic-Klasse mit der Fähigkeit, Infor-mation zu empfangen, zu ändern und weiterzuleiten. Diese Idee wurde vom Inter-faceBuilder bei NeXTSTEP inspiriert, einem Programm, in dem viel vom Informations-fluß bis hin in die anwendungsspezifischen Klassen mit Mausbewegungen ‘‘ver-drahtet’’ werden kann, während die grafische Benutzeroberfläche mit einem grafi-schen Editor entworfen wird. Es lohnt sich allgemein, sich die Lehren dieses Sy-stems anzueignen.

14.4 Implementierung — Ic und seine UnterklassenOffensichtlich ist gate() eine dynamisch gebundene Methode, denn die Unterklas-sen von Ic benützen sie, um Information zu empfangen und zu verarbeiten. Icselbst besitzt out, den Zeiger auf ein anderes Objekt, an das die Information ge-schickt werden muß. Ic ist zwar selbst fast eine abstrakte Basisklasse, aberIc_gate() kann auf out zugreifen und die Information weitergeben:

% Ic gate {%casts

return self -> out ? gate(self -> out, item) : reject;}

Man kommt auf diese Lösung, wenn man Information, nämlich out, verbergen will:Wenn eine Unterklasse in ihrer gate-Methode Information an out weiterschickenmöchte, ruft sie so einfach super_gate() auf.

Page 199: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

189___________________________________________________________________________14.4 Implementierung — ‘‘Ic’’ und seine Unterklassen

wire() ist trivial: Die Methode verbindet ein Ic-Objekt mit einem anderen Ob-jekt, indem sie die Zieladresse in out speichert:

% Ic wire {%casts

self -> out = to;}

Spätestens wenn wir die Multiplexer-Klasse Mux erfinden, erkennen wir, daß auchdie Methode wire() dynamisch gebunden werden muß. Mux ersetzt wire(), umsein Zielobjekt in einem List-Objekt aufzubewahren:

% Mux wire { // Empfaenger hinzufuegen%casts

addLast(self -> list, to);}

Mux_gate() kann verschieden implementiert werden. Auf alle Fälle muß die emp-fangene Information an einige Zielobjekte weitergeleitet werden, aber wir könnenimmer noch die Reihenfolge festlegen, in der wir die Zielobjekte ansprechen, undwir müssen nicht unbedingt aufhören, wenn das erste Ziel die Information akzep-tiert — es bleibt Platz für Unterklassen!

% Mux gate { // an ersten Empfaenger schickenunsigned i, n;enum react result = reject;

%castsn = count(self -> list);for (i = 0; i < n; ++ i){ result = gate(lookAt(self -> list, i), item);

if (result == accept)break;

}return result;

}

Unsere Lösung geht in der Reihenfolge durch die Liste, in der sie angelegt wurde,und wir brechen die Schleife ab, wenn ein Aufruf von gate() die Information mitaccept quittiert.

Wir brauchen LineOut, um einen Computer-Chip zu testen, ohne ihn mit einergrafischen Oberfläche verbinden zu müssen. gate() ist so weitherzig definiert, daßLineOut_gate() kaum mehr als ein Aufruf von puts() ist:

% LineOut gate {%casts

assert(item);puts(item); // hoffentlich eine Zeichenkettereturn accept;

}

LineOut wäre natürlich wesentlich robuster, wenn wir zum Beispiel String-Objekteals Eingabe verwenden würden.

Page 200: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

190___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Die bisher realisierten Klassen kann man tatsächlich schon testen. Das folgen-de Beispiel hello verbindet ein Ic-Objekt mit einem Mux-Objekt und von dort zuerstzu zwei weiteren Ic-Objekten und dann zweimal zu einem LineOut-Objekt. Wirschicken eine Zeichenkette zum ersten Ic-Objekt:

int main (){ void * ic = new(Ic());

void * mux = new(Mux());int i;void * lineOut = new(LineOut());

for (i = 0; i < 2; ++ i)wire(new(Ic()), mux);

wire(lineOut, mux);wire(lineOut, mux);wire(mux, ic);puto(ic, stdout);gate(ic, "hello, world");return 0;

}

Die Ausgabe zeigt die Verbindungen, die mit puto() dargestellt werden, und die Zei-chenkette, die das LineOut-Objekt ausgibt:

$ helloIc at 0x182ccwired to Mux at 0x18354wired to [nil]list List at 0x18440

dim 4, count 4 {Ic at 0x184f0

wired to [nil]Ic at 0x18500

wired to [nil]LineOut at 0x184e0

wired to [nil]LineOut at 0x184e0

wired to [nil]}hello, world

Obwohl das Mux-Objekt zweimal mit dem LineOut-Objekt verbunden ist, wirdhello, world nur einmal ausgegeben, denn das Mux-Objekt leitet seine empfange-ne Information nur so lange weiter, bis ein Aufruf von gate() den Wert accept lie-fert.

Bevor wir die Methoden für Button implementieren können, müssen wir dieEvent-Klasse etwas genauer festlegen. Ein Event-Objekt enthält die Information,die normalerweise von einem Ic-Objekt an ein anderes geschickt wird. Informationvon der Tastatur kann als Zeichenkette repräsentiert werden, aber ein Mausklickoder eine Cursor-Position sehen anders aus. Ein Event-Objekt enthält deshalb eineZahl kind, um die Information zu charakterisieren, und einen Zeiger data, der die ei-gentlichen Werte verbirgt.

Page 201: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

191___________________________________________________________________________14.4 Implementierung — ‘‘Ic’’ und seine Unterklassen

% Event ctor { // new(Event(), 0, "text") etc.struct Event * self = super_ctor(Event(), _self, app);

self -> kind = va_arg(* app, int);self -> data = va_arg(* app, void *);return self;

}

Jetzt können wir Button als Informationsfilter entwerfen: Wenn das empfan-gene Event-Objekt eine Zeichenkette enthält, muß sie dem Text im Knopf gleichen,aber jede andere Art von Information akzeptieren wir ohne weitere Untersuchung,denn sie sollte woanders kontrolliert worden sein. Wenn das Event-Objekt akzep-tiert wird, schickt das Button-Objekt seinen eigenen Text weiter:

% Button ctor { // new(Button(), "text")struct Button * self = super_ctor(Button(), _self, app);

self -> text = va_arg(* app, const char *);return self;

}

% Button gate {%casts

if (item && kind(item) == 0&& strcmp(data(item), self -> text))

return reject;return super_gate(Button(), self, self -> text);

}

Auch diese Klassen kann man schon mit einem kleinen Testprogramm button aus-probieren, in dem ein Button-Objekt mit einem LineOut-Objekt verbunden ist:

int main (){ void * button, * lineOut;

char buf [100];

lineOut = new(LineOut());button = new(Button(), "a");wire(lineOut, button);while (gets(buf)){ void * event = new(Event(), 0, buf);

if (gate(button, event) == accept)break;

delete(event);}return 0;

}

button ignoriert alle Eingabezeilen, bis eine Zeile das Zeichen a enthält, eben denText im Knopf:

$ buttonignoreaa

Page 202: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

192___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Hier wird nur ein a eingegeben, das andere gibt das LineOut-Objekt aus.

LineOut und Button wurden hauptsächlich implementiert, um den Computer-Chip zu testen, bevor er mit einer grafischen Oberfläche verbunden wird. Der Com-puter-Chip Calc kann beliebig kompliziert gezüchtet werden, aber wir fangen nurmit einer sehr primitiven Lösung an: Ziffern werden als Wert in der Anzeige gesam-melt, die arithmetischen Operatoren werden ohne Vorrang möglichst bald aus-geführt, = liefert ein Resultat, C löscht den Wert in der Anzeige und Q beendet dasProgramm durch den Aufruf exit(0);.

Dieser Entwurf kann als endliche Maschine ausgeführt werden. Eine konkreteImplementierung verwendet eine Variable state als Index, der einen von zwei Wer-ten auswählt, und eine Variable op, um den aktuellen Operator aufzubewahren, bisder nächste Wert vollständig eingegeben ist:

%prottypedef int values[2]; // Operanden-Stack

% IcClass Calc: Ic {values value; // linker und rechter Operandint op; // Operatorint state; // Zustand der Maschine

%}

Die folgende Tabelle faßt den Algorithmus zusammen, der codiert werden muß:

Eingabe state value[] op super_gate()

any v[any] *= 10Ziffer

v[any] += Ziffer value[any]

0 → 1 v[1] = 0 Eingabe

1 v[0] op= v[1] Eingabe value[0]+ - * /v[1] = 0

0 v[0] = 0

1 → 0 v[0] op= v[1] value[0]=v[0] = 0

C any v[any] = 0 value[any]

Und das funktioniert wirklich:$ run12 + 34 * 56 = Q112334465562576

Page 203: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

193___________________________________________________________________________14.5 Eine zeichenorientierte Schnittstelle — ‘‘curses’’

ZusammenfassungDie Ic-Klassen sind sehr leicht zu implementieren. Eine triviale LineOut-Klasse undeine Eingabeschleife, die von der Tastatur liest, genügen schon, um ein Calc-Objektzu testen, bevor es in eine komplizierte Schnittstelle eingebaut wird. Wir könntensogar ohne die Klassen Button und Mux auskommen.

Calc kommuniziert mit den Knöpfen und der Anzeige über gate(). Das ist so lo-se gekoppelt, daß ein Calc-Objekt weniger (oder sogar mehr) Nachrichten an dieAnzeige schicken kann, als es selbst empfängt.

Ein Calc-Objekt operiert ganz streng bottom-up, das heißt, es reagiert direkt aufjede Eingabe, die bei Calc_gate() angeliefert wird. Leider schließt dies aus, daß wirdie Methode des rekursiven Abstiegs verwenden, die wir im dritten Kapitel kennen-gelernt haben, oder auch andere syntax-orientierte Mechanismen wie yacc -Gram-matiken. Das ist aber charakteristisch für den auf Nachrichten aufgebauten Entwurf.Rekursiver Abstieg und ähnliche Verfahren beginnen im Hauptprogramm, und sieentscheiden, wann sie eine Eingabe betrachten wollen. Im Gegensatz dazu benut-zen Nachrichten-basierte Anwendungen das Hauptprogramm als Schleife, um Ereig-nisse zu sammeln, und die Objekte müssen dann auf diese Ereignisse so reagieren,wie sie angeliefert werden — deshalb eignen sich Nachrichten-basierte Entwürfegut für grafische Oberflächen und meistens weniger gut für Algorithmen.

Wenn wir auf einer top-down-Lösung, wie rekursivem Abstieg, für Calc beste-hen, müssen wir sie separat ausführen, das heißt, sie muß als Koroutine, thread un-ter Mach oder ähnlichen Betriebssystemen, oder sogar als zweiter Prozeß unterUNIX realisiert werden, und die Nachrichten-Idee muß mit Hilfe von Prozeßkommuni-kation unterlaufen werden.

14.5 Eine zeichenorientierte Schnittstelle — cursescurses ist eine sehr alte Bibliothek, mit der zeichenorientierte Terminals verwaltetwerden, und die mit der termcap- oder terminfo-Datenbasis die komplizierten De-tails der verschiedensten Terminals verbirgt [Sch89]. Ursprünglich hat Ken Arnold inBerkeley die Funktionen aus Bill Joy’s Editor vi entnommen. Inzwischen gibt esmehrere, optimierte Implementierungen, sogar für MS-DOS; einige Implementierun-gen sind frei verfügbar.

Wenn wir unseren Computer-Chip mit curses verbinden wollen, müssen wirKlassen im Stil von LineOut und Button entwickeln und entsprechende Objekte miteinem Calc-Objekt verdrahten. curses realisiert einen Datentyp WINDOW, und esstellt sich heraus, daß wir am besten ein WINDOW für jedes grafische Objekt ver-wenden. Die ursprüngliche Fassung von curses verwendet keine Maus und hatnicht einmal Funktionen, um einen Cursor zum Beispiel mit Hilfe von Pfeiltasten zubewegen. Wir müssen deshalb eine neue Art von Objekt implementieren, das denCursor betreibt und Cursor-Positionen als Event-Objekte an die Knöpfe verschickt.

Wir haben offenbar zwei Möglichkeiten. Wir können eine Unterklasse von Icdefinieren, die den Cursor und die Hauptschleife einer Anwendung verwaltet, sowieUnterklassen von Button und LineOut, um die grafischen Objekte zu realisieren.Jede dieser drei Klassen muß selbst ein WINDOW verwalten. Rechts in der folgen-

Page 204: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

194___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

den Tabelle ist eine Alternative dargestellt, bei der wir von einer Unterklasse von Icausgehen, die ein WINDOW besitzt und den Cursor betreiben kann. Zusätzlich er-zeugen wir zwei weitere Unterklassen, die dann als Aggregate jeweils Button- be-ziehungsweise LineOut-Objekte verwenden können.

Object ObjectIc Ic

Button ButtonCButton

LineOut LineOutCLineOut

Crt CrtCButtonCLineOut

Keine Lösung sieht genau richtig aus. Die erste liegt vielleicht näher bei unserer An-wendung, aber wir verkapseln die Existenz des Datentyps WINDOW nicht in einereinzigen Klasse, und es sieht auch nicht so aus, als ob wir curses so verpacken, daßwir die Klassen beim nächsten Projekt wiederverwenden können. Die zweite Lö-sung verkapselt curses offenbar hauptsächlich in der Klasse Crt, aber die Unterklas-sen müssen Objekte enthalten, die ihrerseits recht anwendungsspezifisch sind, dasheißt, auch hier entwickeln wir höchstwahrscheinlich eine einmalige Lösung.

Bleiben wir doch beim zweiten Entwurf. Wir werden im nächsten Abschnitt se-hen, wie wir zu einem besseren Entwurf kommen können, wenn wir Nachrichtenweiterleiten. Hier sind die neuen Klassen:

KLASSE DATEN METHODEN

Object Wurzelklasse

Ic Basisklasse der Anwendung

Crt Basisklasse für Schirmverwaltungwindow curses-WINDOWrows,cols Größe

makeWindow erzeuge mein windowaddStr zeige Zeichenkette in windowcrtBox zeichne Rahmen um windowgate verwalte Cursor, sende Text oder Position

CLineOut Ausgabefenstergate zeige Text in window

CButton Rahmen mit Text zum Klickenbutton ein Button für Event-Objektex,y meine Position

gate falls Text, an button schickenfalls Position in meinem Bereich,Nullzeiger an button schicken

Um diese Klassen zu implementieren, muß man einigermaßen mit curses umgehenkönnen, deshalb sehen wir uns den Code hier nicht im Detail an. Die curses-Funktionen müssen initialisiert werden — das erledigen wir in Crt_ctor(): Die nöti-

Page 205: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

195___________________________________________________________________________14.5 Eine zeichenorientierte Schnittstelle — ‘‘curses’’

gen curses-Funktionen werden aufgerufen, wenn das erste Crt-Objekt erzeugtwird.

Crt_gate() enthält die Hauptschleife des Programms. Das empfangene Event-Objekt wird ignoriert. Statt dessen liest die Methode so lange von der Tastatur, bisein control-D als Eingabe-Ende entdeckt wird. Die Methode liefert dann reject, unddas Hauptprogramm endet.

Ein paar Eingabezeichen kontrollieren den Cursor. Wenn return gedrückt wird,ruft Crt_gate() den Oberklassen-Selektor super_gate() auf und verschickt einEvent-Objekt mit dem Wert 1 als kind und mit einem Vektor mit der aktuellen Zei-len- und Spaltenposition des Cursors. Alle anderen Zeichen werden als Event-Objekte mit dem Wert 0 als kind und dem Zeichen in einer Zeichenkette verpackt.

Die interessante Klasse ist CButton. Wenn ein Objekt konstruiert wird, er-scheint ein Rahmen auf dem Bildschirm mit dem Namen des Knopfs als Text.

% CButton ctor { // new(CButton(), "text", row, col)struct CButton * self = super_ctor(CButton(), _self, app);

self -> button =new(Button(), va_arg(* app, const char *));

self -> y = va_arg(* app, int);self -> x = va_arg(* app, int);

makeWindow(self, 3, strlen(text(self -> button)) + 4,self -> y, self -> x);

addStr(self, 1, 2, text(self -> button));crtBox(self);return self;

}

Das Window wird groß genug erzeugt, daß der Text mit Leerzeichen und einemRahmen umgeben hineinpaßt. wire() muß so ersetzt werden, daß das interneButton-Objekt verbunden wird:

% CButton wire {%casts

wire(to, self -> button);}

CButton_gate() leitet schließlich Event-Objekte mit Text direkt an den internenKnopf weiter. Enthält der Event eine Position, kontrollieren wir, ob sich der Cursorin unserem Rahmen befindet:

% CButton gate {%casts

if (kind(item) == 1) // kind == 1 ist Position{ int * v = data(item); // data ist Vektor [x, y]

if (v[0] >= self -> x && v[0] < self -> x + cols(self)&& v[1] >= self -> y && v[1] < self -> y + rows(self))

return gate(self -> button, 0);return reject;

}return gate(self -> button, item);

}

Page 206: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

196___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Falls die Position paßt, schicken wir einen Nullzeiger an den internen Knopf, der dar-aufhin seinen eigenen Text weiterschickt.

Wieder können wir die neuen Klassen mit einem einfachen Programm cbuttonausprobieren, bevor wir den ganzen curses-Rechner zusammenbauen:

int main (){ void * crt = new(Crt());

void * lineOut = new(CLineOut(), 5, 10, 40);void * button = new(CButton(), "a", 10, 40);

makeWindow(crt, 0, 0, 0, 0); /* ganzer Schirm */gate(lineOut, "hello, world");

wire(lineOut, button), wire(button, crt);gate(crt, 0); /* Hauptschleife */

return 0;}

Dieses Programm zeichnet in der fünften Schirmzeile den Text hello, world und inder Nähe der Bildschirmmitte einen kleinen Knopf mit dem Text a. Wenn wir denCursor in den Knopf hineinbewegen und return drücken, oder wenn wir irgendwodie Taste a drücken, ändert sich die Anzeige, und wir sehen das a. Das Programmcbutton endet, wenn wir es mit einem Signal abbrechen, oder wenn wir control-Deingeben.

Wenn dieser Test klappt, dann funktioniert auch unser Taschenrechner. Er hatnur mehr Knöpfe und einen Computer-Chip:

int main (){ void * calc = new(Calc());

void * crt = new(Crt());void * lineOut = new(CLineOut(), 1, 1, 12);void * mux = new(Mux());static const struct tbl { const char * nm; int y, x; }

tbl [] = { "C", 0, 15,"1", 3, 0, "2", 3, 5, "3", 3, 10, "+", 3, 15,"4", 6, 0, "5", 6, 5, "6", 6, 10, "-", 6, 15,"7", 9, 0, "8", 9, 5, "9", 9, 10, "*", 9, 15,"Q", 12, 0, "0", 12, 5, "=", 12, 10, "/", 12, 15,

0 };const struct tbl * tp;

makeWindow(crt, 0, 0, 0, 0);wire(lineOut, calc);wire(mux, crt);

for (tp = tbl; tp -> nm; ++ tp){ void * o = new(CButton(), tp -> nm, tp -> y, tp -> x);

wire(calc, o), wire(o, mux);}

gate(crt, 0);return 0;

}

Page 207: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

197___________________________________________________________________________14.6 Eine grafische Schnittstelle — ‘‘Xt’’

Die Lösung ist ganz ähnlich wie die letzte. Ein CButton-Objekt braucht Koordina-ten, deshalb erweitern wir die Tabelle, aus der die Knöpfe erzeugt werden. Wir fü-gen ein Crt-Objekt hinzu, verbinden es mit dem Multiplexer und lassen es dieHauptschleife abwickeln.

ZusammenfassungEs ist nicht arg überraschend, daß wir die Klasse Calc wiederverwendet haben —das ist das mindeste, was wir erwarten können, unabhängig davon, mit welcherStrategie wir die Anwendung entworfen haben. Wir haben aber auch die KlasseButton wiederverwendet, und dank der Basisklasse Ic konnten wir uns völlig daraufkonzentrieren, curses selbst zu zähmen, statt daß wir etwa den Computer-Chip füreine andere Art von Eingaben anpassen mußten.

Der Schönheitsfehler ist, daß wir keine klare Trennung zwischen curses und derKlasse Ic erreicht haben. Unsere Klassenhierarchie zwang uns zu einem Kompro-miß: Wir haben praktisch zwei Ic-Objekte in einem CButton-Objekt zusammenge-baut. Wenn unser nächstes Projekt die Klasse Ic nicht verwendet, können wir denCode nicht wiederverwenden, den wir hier mühsam entwickelt haben, um die De-tails der curses-Bibliothek zu verbergen.

14.6 Eine grafische Schnittstelle — XtDas X Window System (X11) ist der de facto Standard für grafische Benutzerober-flächen bei UNIX und anderen Systemen.* X11 verwaltet ein Terminal mit einem Bit-map-Schirm, einer Maus und einer Tastatur und managt Eingabe und Ausgabe. Xlibist eine Bibliothek von Funktionen, die ein Kommunikationsprotokoll zwischen ei-nem Anwendungsprogramm und dem X11-Server implementieren, der das Terminalkontrolliert.

X11-Programmierung ist recht schwierig, denn die Anwendungsprogramme sol-len sich vernünftig benehmen und die Ressourcen des Servers gemeinsam benüt-zen. Es gibt deshalb das X-Toolkit, eine kleine Klassenhierarchie, die vor allem alsGrundlage für Bibliotheken mit grafischen Objekten gedacht ist. Das Toolkit ist in Cimplementiert. Toolkit-Objekte werden als Widgets bezeichnet.

Die Wurzelklasse des Toolkits ist Object, das heißt, wir müssen unseren Codereparieren und diesen Namen vermeiden. Eine weitere wichtige Klasse im Toolkitist ApplicationShell: Ein Widget aus dieser Klasse bildet normalerweise das Rah-menprogramm, wenn eine Anwendung den X11-Server benützen will.

Das Toolkit enthält selbst keine Klassen mit Widgets, die auf dem Bildschirmsichtbar sind. Es gibt jedoch praktisch überall die Bibliothek Xaw mit den AthenaWidgets, eine ziemlich primitive Erweiterung der Klassenhierarchie des X-Toolkits,die jedoch genügend grafische Funktionalität aufweist, daß wir zu unserem Ta-schenrechner kommen.____________________________________________________________________________________________

* Die Standard-Informationsquelle für X11-Programmierung ist die X Window System Buchserie, die vonO’Reilly and Associates publiziert wird. Für diesen Abschnitt sollte man vor allem die Programmiertechni-ken im Band 4 und die Manualseiten im Band 5 konsultieren. ISBN 0-937175-58-7.

Page 208: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

198___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Die Widgets in einer Anwendung bilden einen Baum. Die Wurzel dieses Baumsist ein ApplicationShell-Widget. Wenn wir mit Xaw arbeiten, kommt als nächstes einBox- oder Form-Widget, denn nur diese können in ihrem Schirmbereich weitereWidgets kontrollieren. Für die Anzeige in unserem Taschenrechner können wir einLabel-Widget aus Xaw benutzen, und ein Knopf kann mit einem Command-Widgetimplementiert werden.

Auf dem Bildschirm erscheint ein Command-Widget als Rahmen mit Text.Wenn der Maus-Cursor den Rahmen betritt oder verläßt, ändert sich die Darstellungdes Rahmens. Wenn ein Mausknopf im Rahmen geklickt wird, ruft das Widget ei-ne Callback-Funktion auf, die vorher registriert werden muß.

Sogenannte Ereignistabellen (translation tables) verbinden Ereignisse, wie einenMausklick oder einen Tastendruck, mit sogenannten Aktionen (action functions), diesich auf das Widget beziehen, auf das der Maus-Cursor gerade zeigt. Commandhat Aktionen, die das Aussehen des Rahmens verändern und die Callback-Funktionaufrufen. Diese Funktionen werden in der Ereignistabelle von Command dazu ver-wendet, die Reaktion auf einen Mausklick zu implementieren. Die Ereignistabelleneines Widgets können geändert oder erweitert werden, das heißt, wir können be-schließen, daß der Tastendruck 0 ein bestimmtes Command-Widget genauso be-einflußt, als ob ein Mausknopf in seinem Rahmen gedrückt wurde.

Sogenannte Beschleuniger (accelerators) sind praktisch eine Umlenkung der Er-eignistabellen von einem Widget zu einem anderen. Tatsächlich können wir, wennsich die Maus in einem Box-Widget befindet und eine Taste wie + gedrückt wird,diesen Tastendruck vom Box-Widget zu einem Command-Widget innerhalb der Boxumlenken und dort so verarbeiten, als ob ein Mausknopf im Command-Widgetselbst gedrückt worden ist.

Fassen wir zusammen: Wir benötigen ein ApplicationShell-Widget als Rahmen-programm, ein Box- oder Form-Widget, das andere Widgets enthalten kann, einLabel-Widget als Anzeige, einige Command-Widgets mit geeigneten Callback-Funk-tionen für unsere Knöpfe, und einige magische Manipulationen sorgen dafür, daßdas Drücken bestimmter Tasten die gleichen Effekte hervorruft wie Mausklicks aufbestimmte Command-Widgets.

Die übliche Technik besteht darin, eigene Klassen zu erzeugen, die mit denKlassen der Toolkit-Hierarchie kommunizieren. Solche Klassen werden als Hüllen(wrappers) für die fremde Hierarchie bezeichnet. Offensichtlich sollte die Hülle sounabhängig wie möglich von allen anderen Randbedingungen sein, so daß wir sie inbeliebigen Toolkit-Projekten benützen können. Eigentlich sollten wir Hüllen für dasganze Toolkit entwickeln, aber um den Rahmen dieses Buchs nicht zu sprengen, isthier nur die minimale Hülle für unseren Taschenrechner:

Page 209: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

199___________________________________________________________________________14.6 Eine grafische Schnittstelle — ‘‘Xt’’

KLASSE DATEN METHODEN

Objct unsere Wurzelklasse (umbenannt)

Xt Basisklasse für X-Toolkit-Hüllenwidget mein X-Toolkit-Widget

makeWidget erzeuge mein WidgetaddAllAcceleratorssetLabel ändere label-RessourceaddCallback registriere Callback-Funktion

(Widget akzeptiert nicht unbedingt)

XtApplicationShell RahmenprogrammmainLoop X11-Event-Schleife

XawBox Hülle für Athena’s BoxXawForm Hülle für Athena’s FormXawLabel Hülle für Athena’s LabelXawCommand Hülle für Athena’s Command

Diese Klassen sind sehr leicht zu implementieren. Sie existieren hauptsächlich, umdie häßlicheren X11- und Toolkit-Aufrufe vor unseren Anwendungen zu verbergen.Im Entwurf hat man einige Freiheitsgrade. Zum Beispiel könnte setLabel() fürXawLabel statt für Xt definiert werden, denn eine neue label-Ressource ist nursinnvoll für XawLabel- und XawCommand-Widgets, aber nicht für Application-Shell, Box oder Form. Wenn wir aber setLabel() für Xt definieren, modellieren wir,wie das Toolkit selbst funktioniert: Widgets werden durch sogenannte Ressourcenkontrolliert, die bei der Erzeugung oder später mit XtSetValues() bereitgestellt wer-den können. Es bleibt dem Widget überlassen, ob es eine bestimmte Ressourcekennt und darauf reagiert, daß für die Ressource ein neuer Wert geliefert wird. Daßman die Werte schicken kann, ist eine Eigenschaft des Toolkits insgesamt, nicht et-wa eines einzelnen Widgets.

Mit dieser Grundlage brauchen wir nur noch zwei weitere Arten von Objekten:Ein XLineOut-Objekt erhält eine Zeichenkette und zeigt sie am Schirm, und einXButton-Objekt verschickt ein Event-Objekt mit Text für einen Mausklick oder Ta-stendruck. XLineOut ist eine Unterklasse von XawLabel, die sich wie LineOut be-nimmt, das heißt, wir müssen gate() irgendwie realisieren.

Xt.d% Class XLineOut: XawLabel {%}

Xt.dc% XLineOut ctor { // new(XLineOut(), parent, "name", "text")

struct XLineOut * self =super_ctor(XLineOut(), _self, app);

const char * text = va_arg(* app, const char *);

gate(self, text);return self;

}

Page 210: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

200___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Drei Argumente müssen angegeben werden, wenn ein XLineOut-Objekt erzeugtwird. Der Oberklassen-Konstruktor benötigt ein übergeordnetes Xt-Objekt, das dasVorgänger-Widget in der Widget-Hierarchie der Anwendung verbirgt. Das neueWidget sollte einen Namen bekommen, damit Ressourcen entsprechend angebo-ten werden können. Außerdem sollte das neue XLineOut-Objekt einen Text erhal-ten, der möglicherweise seine Größe auf den Bildschirm festlegt. Der Konstruktorist sehr tapfer und verwendet einfach gate(), um diesen ersten Text an sich selbstzu schicken.

Da XLineOut nicht von Ic abstammt, kann eine Instanz gate() nicht direkt ak-zeptieren. Der Selektor leitet aber den Aufruf weiter, folglich ersetzen wir for-ward() und erledigen dort das, was normalerweise die Methode gate() tun würde,wenn XLineOut eine hätte:

% XLineOut forward {%casts

if (selector == (Method) gate){ va_arg(* app, void *);

setLabel((void *) self, va_arg(* app, const void *));* (enum react *) result = accept;

}else

super_forward(XLineOut(), _self, result,selector, name, app);

}

Wir können nicht sicher sein, daß jeder Aufruf von XLineOut_forward() ein ver-kleideter Aufruf von gate() ist. Die Methode forward() sollte immer nachsehenund nur auf geplante Aufrufe reagieren. Unerwünschte Aufrufe kann man natürlichmit super_forward() auf der Vererbungskette aufwärts schicken.

Genau wie new() ist auch der Selektor forward() mit einer variablen Parameter-liste definiert. Der Selektor kann aber nur einen Zeigerwert vom Typ va_list an dieeigentliche Methode übergeben und auch an den Oberklassen-Selektor muß so einWert übergeben werden. Damit Argumentlisten leicht von mehreren verkettetenMethoden gemeinsam benutzt werden können, speziell für den Metaklassen-Kon-struktor, generiert ooc Code, der einen Zeiger auf einen Zeiger auf die Argumentli-ste übergibt, das heißt, der generierte Parameter ist va_list * app.

Ein triviales Beispiel, das demonstriert, wie eine Nachricht mit gate() an einXLineOut-Objekt weitergeleitet wird, ist das Testprogramm xhello:

void main (int argc, char * argv []){ void * shell = new(XtApplicationShell(), & argc, argv);

void * lineOut = new(XLineOut(), shell, 0, "hello, world");

mainLoop(shell);}

Das Programm zeigt ein Window mit dem Text hello, world und muß dann mit ei-nem Signal abgebrochen werden. Hier haben wir dem Widget im XLineOut-Objektkeinen expliziten Namen gegeben, denn wir übergeben keine Ressourcen.

Page 211: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

201___________________________________________________________________________14.6 Eine grafische Schnittstelle — ‘‘Xt’’

XButton ist eine Unterklasse von XawCommand, damit wir eine Callback-Funktion registrieren können, um einen Mausklick oder Tastendruck zu empfangen:

Xt.d% Class XButton: XawCommand {

void * button;%}

Xt.dc% XButton ctor { // new(XButton(), parent, "name", "text")

struct XButton * self = super_ctor(XButton(), _self, app);const char * text = va_arg(* app, const char *);

self -> button = new(Button(), text);setLabel(self, text);addCallback(self, tell, self -> button);return self;

}

XButton hat die gleichen Konstruktor-Argumente wie XLineOut: ein Xt-Objekt alsVorgänger, einen Widget-Namen und den Text, der auf dem Knopf stehen soll. Na-me und Text sind zwei verschiedene Argumente, denn die Operatoren unseres Ta-schenrechners zum Beispiel eignen sich nicht als Komponenten in den Pfadnamender Ressourcen.

Der interessante Teil ist die Callback-Funktion. Wir lassen ein XButton-Objektein Button-Objekt besitzen und sorgen dafür, daß die Callback-Funktion tell() einenNullzeiger mit gate() an das Button-Objekt schickt:

static void tell (Widget w, XtPointer client_data,XtPointer call_data)

{gate(client_data, NULL);

}

client_data wird zusammen mit der Callback-Funktion angemeldet, damit das Wid-get diese Information später liefert. Wir verwenden diesen Zeiger, um auf das Zielfür den Aufruf von gate() zu verweisen.

Wir könnten auch ohne das interne Button-Objekt auskommen, denn wir könn-ten XButton sich selbst mit einem anderen Objekt verdrahten lassen; client_datamüßte dann auf einen Zeiger auf dieses Ziel und einen Zeiger auf den Text zeigen,und dann könnte tell() den Text direkt zum Ziel schicken. Es ist aber natürlich einfa-cher, die Funktionalität von Button wiederzuverwenden, insbesondere, weil dannauch XButton Text über einen weitergeleiteten Aufruf von gate() empfangen undan das interne Button-Objekt zum Filtern übergeben kann.

Weiterleitung von Nachrichten ist auch der Schlüssel zu XButton: Das interneButton-Objekt ist unzugänglich, aber es muß einen Aufruf von wire() empfangenkönnen, der ursprünglich an XButton gerichtet ist:

Page 212: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

202___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

% XButton forward {%casts

if (selector == wire)wire(va_arg(* app, void *), self -> button);

elsesuper_forward(XButton(), _self, result,

selector, name, app);}

Wenn wir die Implementierungen der Methode forward für XLineOut und XButtonvergleichen, sehen wir, daß forward() zwar self mit dem Attribut const empfängt,das Attribut im Fall von XLineOut_forward() aber umgeht. Wer gate() implemen-tiert, muß wissen, ob das korrekt ist.

Wieder können wir auch XButton mit einem einfachen Programm xbutton aus-probieren. Dieses Programm setzt ein XLineOut-Objekt und ein XButton-Objekt inein XawBox-Objekt und ein weiteres Paar in ein XawForm-Objekt. Die beiden um-gebenden Widgets werden in einem weiteren XawBox-Objekt angeordnet:

void main (int argc, char * argv []){ void * shell = new(XtApplicationShell(), & argc, argv);

void * box = new(XawBox(), shell, 0);void * composite = new(XawBox(), box, 0);void * lineOut = new(XLineOut(), composite, 0, "-long-");void * button = new(XButton(), composite, 0, "a");

wire(lineOut, button);puto(button, stdout); /* Box bewegt Nachfolger */

composite = new(XawForm(), box, "form");lineOut = new(XLineOut(), composite,"lineOut", "-long-");button = new(XButton(), composite, "button", "b");

wire(lineOut, button);puto(button, stdout); /* Form fixiert Nachfolger */

mainLoop(shell);}

Das Resultat sieht auf dem Schirm ungefähr so aus:

-long-

a

-long- b

Wenn der Knopf a in der oberen Hälfte gedrückt wird, empfängt das XLineOut-Objekt den Text a und stellt ihn dar. Das Box-Widget, das hier die Anordnung aufdem Schirm kontrolliert, erlaubt, daß sich die Größe des Label-Widgets in Abhän-gigkeit vom Text verändert, das heißt, der obere Rahmen ändert sich und zeigt zwei

Page 213: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

203___________________________________________________________________________14.6 Eine grafische Schnittstelle — ‘‘Xt’’

Quadrate, die jeweils den Text a enthalten. Der Knopf mit dem Text b wird von ei-nem Form-Widget kontrolliert, wobei folgende Ressource

*form.button.fromHoriz: lineOut

die Anordnung kontrolliert. Das Form-Widget erlaubt auch dann nicht, daß das un-tere Rechteck sein Aussehen ändert, wenn b gedrückt wird und der kurze Text b imXLineOut-Objekt erscheint.

Das Testprogramm demonstriert, daß XButton mit Mausklicks und wire() funk-tioniert, deshalb können wir jetzt auch den Taschenrechner xrun verdrahten:

void main (int argc, char * argv []){ void * shell = new(XtApplicationShell(), & argc, argv);

void * form = new(XawForm(), shell, "form");void * lineOut = new(XLineOut(), form, "lineOut",

"........");void * calc = new(Calc());static const char * const cmd [] = { "C", "C",

"1", "1", "2", "2", "3", "3", "a", "+","4", "4", "5", "5", "6", "6", "s", "-","7", "7", "8", "8", "9", "9", "m", "*","Q", "Q", "0", "0", "t", "=", "d", "/", 0 };

const char * const * cpp;

wire(lineOut, calc);for (cpp = cmd; * cpp; cpp += 2){ void * button = new(XButton(), form, cpp[0], cpp[1]);

wire(calc, button);}addAllAccelerators(form);mainLoop(shell);

}

Das Programm ist sogar noch einfacher als die curses-Version, denn die Tabelleenthält nur den Widget-Namen und den Text für jeden Knopf. Die geometrischeAnordnung der Widgets wird durch Ressourcen festgelegt:

*form.C.fromHoriz: lineOut*form.1.fromVert: lineOut*form.2.fromVert: lineOut*form.3.fromVert: lineOut*form.a.fromVert: C*form.2.fromHoriz: 1*form.3.fromHoriz: 2*form.a.fromHoriz: 3...

Die Ressourcendatei Xapp enthält auch die Beschleuniger, die mit addAllAccelera-tors() geladen werden:

*form.C.accelerators: <KeyPress>c: set() notify() unset()*form.Q.accelerators: <KeyPress>q: set() notify() unset()*form.0.accelerators: :<KeyPress>0: set() notify() unset()...

Page 214: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

204___________________________________________________________________________14 Nachrichten weiterleiten — Ein GUI-Rechner

Der Taschenrechner kann zum Beispiel mit folgendem Kommando von der Bourne-Shell aus gestartet werden und liest dann die Ressourcen aus Xapp:

$ XENVIRONMENT=Xapp xrun -display unix:0

14.7 ZusammenfassungIn diesem Kapitel haben wir einen objekt-orientierten Entwurf für einen einfachenTaschenrechner mit einer grafischen Benutzeroberfläche betrachtet. Die Entwurfs-technik CRC, die am Ende von Abschnitt 14.3 skizziert wurde, führt zu einigen Klas-sen, die in jeder der drei Lösungen unverändert eingesetzt werden können.

Die erste Lösung testet den eigentlichen Computer-Chip ohne eine grafischeSchnittstelle. Hier erlaubt die Verkapselung als Klasse einen einfachen Testaufbau.Wenn die Klasse Calc funktioniert, können wir uns ganz auf die Feinheiten der je-weiligen Grafikbibliothek konzentrieren und müssen den zentralen Algorithmus un-serer Anwendung nicht mehr ändern.

Für curses wie X11 benötigen wir einige Klassen als Hülle, um die externe Bi-bliothek mit unserer Klassenhierarchie zu verbinden. Das curses-Beispiel demon-striert, daß wir Kompromisse schließen müssen, wenn wir keine Nachrichten wei-terleiten: Eine Hülle, die vielleicht eher für das nächste Projekt wiederverwendetwerden kann, funktioniert nicht so gut im Zusammenhang mit einer existenten, an-wendungsorientierten Klassenhierarchie; eine Hülle, die gut zu unserer Aufgaben-stellung paßt, weiß zu viel über unsere Anwendung, als daß sie allgemein zum Um-gang mit curses wiederverwendet werden wird.

Die Lösung für X11 zeigt, wie bequem man mit weitergeleiteten Nachrichten ar-beiten kann. Hüllklassen verbergen die internen Aspekte von X11 und den Toolkit-Widgets fast völlig. Problem-orientierte Klassen wie XButton verbinden die nötigeFunktionalität aus der Hülle mit der Ic-Klasse, die wir für unseren Taschenrechnerentwickelt haben. Durch die Weiterleitung von Nachrichten funktionieren Klassenwie XButton, als ob sie von Ic abstammen. In diesem Beispiel können Objektedurch weitergeleitete Nachrichten sich so benehmen, als ob sie gleichzeitig zu zweiKlassen gehören, aber wir erreichen das ohne den hohen Aufwand und die Komple-xität der mehrfachen Vererbung, wie sie C++ unterstützt.

Nachrichten können sehr leicht weitergeleitet werden. Wir müssen dazu nurdie Generierung der Selektoren in den entsprechenden Reports für ooc modifizie-ren, so daß unverständliche Selektor-Aufrufe an eine neue dynamisch gebundeneMethode forward() weitergeleitet werden, die Klassen wie XButton ersetzen, umweitergeleitete Nachrichten zu empfangen und eventuell umzulenken.

14.8 ÜberlegungenFür Freunde zeichenorientierter Terminals ist eine Hülle für die curses-Bibliothek ei-ne interessante Aufgabe. Unser Taschenrechner-Experiment kann mit OSF/Motifoder einem anderen Toolkit für X11 wiederholt werden.

Page 215: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

205___________________________________________________________________________14.8 Überlegungen

Beschleuniger sind vielleicht nicht der offensichtlichste Weg, um einen Tasten-druck in eine Eingabe für unseren Taschenrechner zu verwandeln. Wahrscheinlichdenkt man zuerst an eigene Aktionen. Es zeigt sich aber, daß eine eigene Aktionzwar das Widget kennt, auf das sie sich bezieht, aber sie hat keinen vernünftigenVerweis vom Widget zu unserer Hülle. Entweder jemand übersetzt das X-Toolkitmit einem zusätzlichen Zeiger für Benutzerdaten in der Object-Instanz-Struktur,oder wir müssen Unterklassen für einige Toolkit-Widgets entwickeln, um einen der-artigen Zeiger einzufügen. Mit einem solchen Zeiger können wir aber interessanteLösungen mit eigenen Aktionen und gate() konstruieren.

Die Idee für gate() und wire() wurde mehr oder weniger aus NeXTSTEP ent-nommen. Dort kann aber eine Klasse mehr als einen Ausgang besitzen, das heißt,mehrere Zeiger auf andere Objekte, und beim Verdrahten werden sowohl der Aus-gang als auch die Methode angegeben, die beim Empfänger aufgerufen werdensoll.

Wenn wir die Abschnitte 5.5 und 11.4 vergleichen, stellen wir fest, daß Varwirklich von Node und Symbol erben sollte. Mit forward() könnten wir ohne Valund seine Unterklassen auskommen.

Page 216: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 217: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

207___________________________________________________________________________

Anhang AANSI-C Programmiertips

C wurde ursprünglich von Dennis Ritchie im Anhang von [K&R83] definiert. DerANSI-C Standard [ANSI] erschien ungefähr zehn Jahre später und führte bestimmteÄnderungen und Erweiterungen ein. Die Unterschiede faßt Anhang C von [K&R89]sehr knapp und präzise zusammen. Unser Stil, in ANSI-C objekt-orientiert zu pro-grammieren, beruht auf einigen der Erweiterungen. Als Hilfe für klassische C-Pro-grammierer erklärt dieser Anhang die Innovationen von ANSI-C, die für dieses Buchwichtig sind. Der Anhang ist keineswegs eine Definition der ProgrammierspracheANSI-C.

A.1 Namen und GeltungsbereichANSI-C legt fest, daß Namen fast beliebig lang sein können. Namen, die mit einemUnterstrich beginnen, sind für Bibliotheken vorgesehen, das heißt, sie sollten nichtin Benutzerprogrammen verwendet werden. Global vereinbarte Namen kann manmit static in einer Quelldatei verstecken:

static int f (int x) { ... } nur in Quelldatei sichtbarint g; im ganzen Programm sichtbar

Vektornamen sind konstante Adressen, die man zur Initialisierung von Zeigern ver-wenden kann, und zwar selbst dann, wenn ein Vektor auf sich selbst Bezug nimmt:

struct table { struct table * tp; }v [] = { v, v+1, v+2 };

Es ist nicht völlig klar, wie man einen Vorwärtsverweis auf ein Objekt codiert, dasnoch immer in einer Quelldatei verborgen sein soll. Folgendes ist offenbar korrekt:

extern struct x object; Vorwärtsverweisf() { object = value; } benützenstatic struct x object; verborgene Definition

A.2 FunktionenANSI-C erlaubt — verlangt aber nicht zwingend — daß bei der Vereinbarung vonFunktionen die Parameter direkt in der Parameterliste deklariert werden. Funktions-deklarationen legen damit auch die Typen ihrer Parameter fest. Optional und ohneEinfluß auf die Funktionsdefinition können auch die Parameternamen angegebenwerden:

double sqrt (); alte Fassungdouble sqrt (double); moderne Fassungdouble sqrt (double x); ... mit Parameternamenint getpid (void); keine Parameter, moderne Fassung

Page 218: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

208___________________________________________________________________________Anhang A ANSI-C Programmiertips

Wenn ein moderner Prototyp bekannt ist, wandelt ein ANSI-C Compiler bei Bedarfund nach Möglichkeit die Argumentwerte in die Parametertypen um. Bei der Funk-tionsdefinition sind beide Varianten erlaubt:

double sqrt (double arg) moderne Fassung{ ... }

double sqrt (arg) alte Fassungdouble arg;

{ ... }Es gibt genaue Regeln zum Zusammenspiel zwischen modernen und alten Prototy-pen und Definitionen, aber sie sind kompliziert und fehleranfällig. Man sollte unbe-dingt nur mit modernen Prototypen und Definitionen arbeiten.

Mit der Option −Wall gibt der GNU-C Compiler eine Warnung aus, wenn eineFunktion aufgerufen wird, die noch nicht vereinbart ist.

A.3 Generische Zeiger — void *Jeder Zeigerwert kann einem Zeiger vom Typ void * zugewiesen werden und um-gekehrt, abgesehen von const-Attributen. Dabei ändert sich der Zeigerwert nicht.Letztlich schaltet man dadurch aber Typprüfung im Compiler aus:

int iv [] = { 1, 2, 3 };int * ip = iv; ok, gleicher Typvoid * vp = ip; ok, beliebiger Zeiger an void *double * dp = vp; ok, void * an beliebigen Zeiger

Zeiger kann man mit dem Format %p aus- und (prinzipiell) auch eingeben. Der zu-gehörige Typ is void * und damit aber jeder Zeigertyp:

void * vp;printf("%p\n", vp); Wert darstellenscanf("%p", & vp); Wert einlesen

Mit void * kann keine Arithmetik betrieben werden:

void * p, ** pp;p + 1 falsch!pp + 1 ok, zeigt auf Zeiger

Das folgende Bild illustriert diese Situation:

••

p

pp void

Page 219: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

209___________________________________________________________________________A.4 ‘‘const’’

A.4 constconst ist ein Attribut, mit dem man anzeigt, daß der Compiler eine Zuweisung nichterlauben soll. Das ist deutlich verschieden von wirklich konstanten Werten. Initiali-sierung ist erlaubt, bei lokalen Variablen auch mit variablen Werten:

int x = 10;int f () { const int xsave = x; ... }

Mit Hilfe von expliziten Umwandlungen kann man das allerdings immer so umge-hen, daß der Compiler keine Fehlermeldungen ausgibt:

const int cx = 10;(int) cx = 20; falsch!* (int *) & cx = 20; prinzipiell möglich

Diese Umwandlungen sind insbesondere bei der Zuweisung von Zeigern nötig:

vp • const voidconst void * vp;

int * ip;int * const p = ip; ok, bei lokaler Variable

vp = ip; ok, blockt Zuweisungip = vp; falsch, ermöglicht Zuweisungip = (void *) vp; ok, mit Gewalt* (const int **) & ip = vp; ok, Overkillp = ip; falsch, Zeiger ist geblockt* p = 10; ok, Ziel ist nicht geblockt

Mit der Ausnahme, daß const auch vor dem Typ einer Vereinbarung stehen kann,bindet const nach links:

int const v [10]; 10 konstante Vektorelementeconst int * const cp = v; konstanter Zeiger auf konstante Werte

Man verwendet const, um die Absicht anzuzeigen, daß man einen Wert nach Initia-lisierung oder innerhalb einer Funktion nicht ändern will. Dies ist vor allem dannnützlich, wenn ein Zeiger an eine Funktion übergeben wird:

char * strcpy (char * ziel, const char * quelle);Globale Objekte, die vollständig mit const vereinbart sind, darf der Compiler in ei-nem nicht schreibbaren Segment anlegen. Das heißt zum Beispiel, daß die Kompo-nenten einer Struktur const einerben:

const struct { int i; } c;c.i = 10; falsch!

Dies schließt auch die dynamische Initialisierung des folgenden Zeigers aus:

void * const String;Es ist nicht klar, was es bedeutet, wenn eine Funktion ein const-Resultat liefert.GNU-C nimmt dann an, daß die Funktion keine Nebeneffekte hat und nur ihre Argu-

Page 220: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

210___________________________________________________________________________Anhang A ANSI-C Programmiertips

mente selbst — nicht die Werte hinter Zeigern — ansieht. Aufrufe solcher Funktio-nen können dann wegoptimiert werden. Sonst sollte man const nicht verwenden.

Da Zeiger auf const-Werte nicht ungeschützten Zeigern zugewiesen werdenkönnen, hat ANSI-C eine recht merkwürdige Deklaration für bsearch():

void * bsearch (const void * key,const void * table, size_t nel, size_t width,int (* cmp) (const void * key, const void * elt));

Zwar wird table[] mit unveränderlichen Elementen importiert, aber das Resultatzeigt so auf ein Element, daß es modifiziert werden darf.

Als Faustregel sollten Parameter einer Funktion genau dann als Zeiger aufconst-Objekte deklariert werden, wenn diese Objekte über die Zeiger nicht verän-dert werden. Gleiches gilt für Zeiger-Variablen. Das Resultat einer Funktion solltepraktisch nie mit const vereinbart werden.

A.5 typedef und consttypedef definiert keine Makros. Im Umfeld von typedef ist const möglicherweiseanders gebunden, als man vielleicht vermutet:

const struct Class { ... } * p; Strukturinhalt geschützttypedef struct Class { ... } * ClassP;const ClassP cp; Strukturinhalt offen, Zeiger geschützt

Wie man die Elemente einer Matrix schützt und übergibt, bleibt ein Puzzle:

main (){ typedef int matrix [10][20];

matrix a;int b [10][20];int f (const matrix);int g (const int [10][20]);f(a);f(b);g(a);g(b);

}Es gibt Compiler, die keinen der Aufrufe erlauben, andere erlauben alle...

A.6 StrukturenStrukturen fassen Komponenten mit verschiedenen Typen zusammen. Dabei kön-nen Strukturen, Komponenten und Variablen gleiche Namen haben:

struct u { int u; double v; } u;struct v { double u; int v; } * vp;

Die Auswahl von Strukturkomponenten erfolgt für Variablen mit dem Operator . undfür Zeiger auf Strukturen mit −> als Operator

u.u = vp -> v;

Page 221: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

211___________________________________________________________________________A.7 Zeiger auf Funktionen

Ein Zeiger auf eine Struktur kann vereinbart werden, ohne daß die Struktur schonvereinbart ist. Man kann eine Struktur vereinbaren, ohne dabei Objekte zu vereinba-ren:

struct w * wp;struct w { ... };

Eine Struktur kann eine Struktur enthalten:

struct a { int x; };struct b { ... struct a y; ... } b;

Zum Zugriff muß man dann die komplette Folge von Komponenten angeben:

b.y.x = 10;Die erste Komponente einer Struktur beginnt unmittelbar am Anfang der Struktur —deshalb kann man Strukturen verlängern und verkürzen:

struct a { int x; };struct c { struct a a; ... } c, * cp = & c;struct a * ap = & c.a;

assert((void *) ap == (void *) cp);ANSI-C erlaubt weder implizite Umwandlungen von Zeigern auf verschiedene Struk-turen noch direkten Zugriff auf die Komponenten einer inneren Struktur:

ap = cp; falsch!c.x, cp -> x beides falsch!cp -> a.x ok, voll qualifiziert((struct a *) cp) -> x ok, explizit umgewandelt

A.7 Zeiger auf FunktionenAus der Deklaration einer Funktion entsteht die Vereinbarung eines Zeigers auf eineFunktion, indem man eine Verweisebene zum Funktionsnamen hinzufügt, wobei derVorrang durch Klammern erzwungen werden muß:

void * f (void *); Funktion mit Zeiger als Resultatvoid * (* fp) (void *) = f; Zeiger auf derartige Funktion

Zur Initialisierung verwendet man meistens vorher vereinbarte Funktionsnamen.Beim Aufruf kann man Funktionsnamen und Zeiger gleich behandeln:

int x;f (& x); Aufruf mit Namefp (& x); Aufruf mit Zeiger, ANSI-C(* fp)(& x); Aufruf mit Zeiger, klassisch

Zeiger auf Funktionen können auch Komponenten von Strukturen sein:

struct Class { ...void * (* ctor) (void * self, va_list * app);

... } * cp, ** cpp;Beim Aufruf hat −> Vorrang vor dem Funktionsaufruf, aber nicht vor einer Verweis-operation, das heißt, im zweiten Beispiel sind die Klammern notwendig:

Page 222: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

212___________________________________________________________________________Anhang A ANSI-C Programmiertips

cp -> ctor ( ... );(* cpp) -> ctor ( ... );

A.8 PräprozessorANSI-C expandiert #define nicht mehr rekursiv, folglich kann man Funktionsaufrufedurch gleichnamige Makros verbergen oder vereinfachen:

#define malloc(typ) (typ *) malloc(sizeof(typ))int * p = malloc(int);

ANSI-C erkennt Aufrufe parametrisierter Makros nur, wenn der Makroname vor ei-ner linken Klammer steht, folglich kann man die Erkennung parametrisierter Makrosin Funktionsköpfen mit Klammern abschalten:

#include <stdio.h> definiert zum Beispiel putchar(ch)

int (putchar) (int ch) { ... } Name wird nicht ersetzt

Außerdem kollidiert die Definition von parametrisierten Makros nicht mehr mitgleichnamigen Variablen:

#define x(p) (((const struct Object *)(p)) -> x)

int x = 10; Name wird nicht ersetzt

A.9 Verifikation — assert.h#include <assert.h>

assert( bedingung );Dieser Makroaufruf bricht das Programm mit einer entsprechenden Fehlermeldungab, wenn die bedingung nicht zutrifft.

Mit der Option −DNDEBUG kann man alle Aufrufe von assert() bei der Überset-zung entfernen. bedingung sollte deshalb keinesfalls Nebeneffekte enthalten.

A.10 Globale Sprünge — setjmp.h#include <setjmp.h>jmp_buf onError;int val;

if (val = setjmp(onError))Fehlerbehandlung

elseerster Aufruf

...longjmp(onError, val);

Mit diesen Funktionen kann man einen globalen Sprung von einer Funktion zu eineranderen, früher aufgerufenen und noch immer aktivierten Funktion realisieren.Beim ersten Aufruf markiert setjmp() die Situation im jmp_buf und liefert Null als

Page 223: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

213___________________________________________________________________________A.11 Variable Parameterlisten — ‘‘stdarg.h’’

Resultat. Mit longjmp() kehrt man dann später zu dieser Situation zurück; dann lie-fert setjmp() den Wert als Resultat, der bei longjmp() angegeben wird (oder eins,falls null angegeben wurde).

Es gibt Randbedingungen: Der Kontext von setjmp() muß noch aktiv sein, erdarf nicht zu kompliziert sein, Variablen werden nicht restauriert, ein Rücksprungzum Aufruf von longjmp() ist nicht mehr möglich etc. Das Verfahren berücksichtigtjedoch Rekursionsebenen dynamisch: longjmp() kehrt zu der Rekursionsebenezurück, in der setjmp() aufgerufen wurde.

A.11 Variable Parameterlisten — stdarg.h#include <stdarg.h>

void fatal (const char * fmt, ... ){ va_list ap;

int code;

va_start(ap, fmt); letzter expliziter Parametercode = va_arg(ap, int); nächster Argumentwertvprintf(fmt, ap);va_end(ap); reinitialisierenexit(code);

}Endet die Parameterliste in einem Prototyp und in der Funktionsdefinition mit dreiPunkten, darf die vereinbarte Funktion mit beliebig vielen Argumenten aufgerufenwerden. Da die Anzahl der übergebenen Argumente nicht verfügbar ist, muß siemit Hilfe eines Parameters oder eines abschließenden Werts feststellbar sein.

Mit den Makros aus stdarg.h kann man die Parameterliste abarbeiten. va_listist ein Typ zur Bearbeitung der Liste. Mit va_start() wird eine va_list-Variable initia-lisiert; dabei muß exakt der letzte explizite Parametername angegeben werden. Mitva_arg() holt man jeweils den nächsten Argumentwert. Mit va_end() muß man dieVerarbeitung beenden; danach kann man die Werteliste erneut bearbeiten.

Werte vom Typ va_list kann man an andere Funktionen übergeben. Insbeson-dere gibt es Varianten der printf-Funktionen, die va_list an Stelle der normalen Wer-teliste akzeptieren.

Die Werte im variablen Teil der Argumentliste unterliegen den alten Regeln:Ganzzahlige Werte werden als int oder long, Gleitkomma-Werte als double überge-ben. Der gewünschte Typ, der als zweites Argument von va_arg() angegebenwird, darf syntaktisch nicht zu kompliziert sein — im Zweifelsfall hilft typedef.

A.12 Datentypen — stddef.hIn stddef.h sind einige Datentypen vereinbart, die je nach Plattform und Compilerverschieden sein können. Sie beschreiben die Resultate bestimmter Operationen:

size_t Resultat von sizeofptrdiff_t Differenz zweier Zeiger

Page 224: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

214___________________________________________________________________________Anhang A ANSI-C Programmiertips

Außerdem gibt es einen Makro, der den Abstand einer Komponente vom Anfang ei-ner Struktur berechnet:

struct s { ... int a; ... };offsetof(struct s, a) liefert size_t Wert

A.13 Speicherverwaltung — in stdlib.hvoid * calloc (size_t nel, size_t len);void * malloc (size_t size);void * realloc (void * p, size_t size);void free (void * p);

Diese Funktionen sind alle in stdlib.h deklariert. calloc() liefert eine mit Null initiali-sierte Speicherfläche aus nel Elementen mit jeweils der Länge len, und malloc()liefert eine uninitialisierte Speicherfläche der Länge size. Die Funktion realloc() ak-zeptiert eine derartige Speicherfläche und sorgt dafür, daß anschließend die Längesize verfügbar ist — dabei wird die Fläche möglicherweise verlagert. free() gibt ei-ne derartige Fläche wieder frei — akzeptiert aber auch einen Nullzeiger.

A.14 Memory-Funktionen — in string.hZusätzlich zu den bekannten String-Funktionen definiert string.h Funktionen zur Ma-nipulation von Speicherblöcken, insbesondere:

void * memcpy (void * to, const void * from, size_t len);void * memmove (void * to, const void * from, size_t len);void * memset (void * area, int value, size_t len);

memcpy() und memmove() kopieren eine Fläche, dabei dürfen sich für die Funkti-on memmove() die Flächen bei from und to überlappen. memset() initialisiert eineFläche mit einem Byte-Wert.

Page 225: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

215___________________________________________________________________________

Anhang BDer ooc-Präprozessorawk Programmiertips

awk wurde ursprünglich mit der Edition Sieben des UNIX-Systems ausgeliefert. Et-wa 1985 erweiterten die Autoren die Sprache entscheidend, und sie beschriebendas Resultat in [AWK88]. Heute entwickelt sich ein POSIX-Standard, und die neueSprache ist in einer Reihe von Implementierungen verfügbar, zum Beispiel als nawkfür System V; als awk , aus den gleichen Quellen entwickelt, mit den MKS-Tools fürMSDOS; als gawk von der Free Software Foundation (GNU). Dieser Anhang setztgrundsätzliche Kenntnisse der (neuen) Programmiersprache awk voraus und gibteinen Überblick zur Implementierung des ooc-Präprozessors. Die Implementierungvon ooc verwendet verschiedene Möglichkeiten, die der POSIX-Standard für awk bie-tet, und sie wurde mit gawk realisiert.

B.1 Architekturooc ist als Shell-Skript implementiert, das ein awk -Programm lädt und ausführt.Das Shell-Skript vereinfacht die Übergabe von Argumenten aus der Kommandozeilevon ooc an das awk -Programm, und es erlaubt die zentrale Speicherung der ver-schiedenen Module.

Das awk -Programm sammelt eine Datenbasis von Information über Klassenund Methoden aus den Klassenbeschreibungsdateien, und es erzeugt C-Code ausder Datenbasis für Schnittstellen- und Repräsentierungsdateien und für Metho-denköpfe, Selektoren, Import von Parametern und Initialisierung in den Implemen-tierungsdateien. Das awk -Programm beruht auf zwei Entwurfskonzepten: Modula-risierung und Reportgenerierung.

Ein Modul enthält eine Reihe von Funktionen und eine BEGIN-Aktion, die die glo-balen Daten definiert, die in den Funktionen manipuliert werden. In einem awk -Programm kann man zwar keine Information verbergen, aber die Module sind trotz-dem in verschiedenen Dateien implementiert, um die Pflege zu vereinfachen. DasShell-Skript ooc kann die Environment-Variable AWKPATH verwenden, um die Mo-duldateien an einer zentralen Stelle zu finden.

Alle Arbeit erfolgt nur von BEGIN-Aktionen aus, die awk in der Reihenfolge ihresAuftretens ausführt, deshalb muß main.awk zuletzt geladen werden, denn dieserModul bearbeitet die vom Shell-Skript ooc übergebene Kommandozeile.

Musteranweisungen werden nicht verwendet, denn damit können ohnehinnicht alle Dateien bearbeitet werden, da ooc für jede Klassenbeschreibung alle Klas-senbeschreibungsdateien konsultiert, die ihr in der Klassenhierarchie vorausgehen.Der Algorithmus, um Zeilen zu lesen, Kommentare zu entfernen und Zeilen zu ver-schmelzen, ist in einer einzigen Funktion get() in io.awk implementiert. Wenn Mu-steranweisungen verwendet würden, müßte dieser Algorithmus mit Mustern dupli-ziert werden.

Page 226: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

216___________________________________________________________________________Anhang B Der ‘‘ooc’’-Präprozessor — ‘‘awk’’ Programmiertips

Die Datenbasis kann betrachtet werden, wenn bestimmte Debugger-Moduleals Teil des awk -Programms geladen werden. Diese Debugger-Module werden vonMusteranweisungen aus kontrolliert, das heißt, sie kommen erst zum Einsatz, nach-dem die Kommandozeile von ooc über die BEGIN-Aktionen abgearbeitet ist. Debug-ger-Anweisungen werden als Standard-Eingabe vorgelegt und von den Musteran-weisungen ausgeführt.

Die normale Ausgabe wird nur durch die Interpretation von Reports erzeugt.Das Entwurfsziel ist, daß das awk -Programm selbst möglichst wenig Informationüber den generierten Code enthält. Die Codegenerierung sollte vollständig durchÄndern der Reports kontrolliert werden. Da das Shell-Skript ooc das Ersetzen vonReportdateien erlaubt, kann der Anwender — mindestens theoretisch — die ge-samte Ausgabe modifizieren, ohne das awk -Programm ändern zu müssen.

B.2 Dateimanagement — io.awkDieser Modul ist für den Zugriff auf alle Dateien verantwortlich. Er verwaltet denVektor FileStack[] mit Name und Zeilennummer aller offenen Dateien.openFile(fnm) legt FILENAME und FNR auf diesem Stack ab und stellt mit system()fest, ob eine Datei fnm lesbar ist. Der vollständige Name wird dann in FILENAMEeingetragen, und FNR wird neu initialisiert. Die Funktion get() liest aus der DateiFILENAME und liefert entweder eine komplette Eingabezeile ohne Kommentare undfertig verschmolzen oder den speziellen Wert EOF am Dateiende. Dieser Wert be-ginnt mit %, um bestimmte Schleifen zu vereinfachen. closeFile() beendet den Zu-griff auf FILENAME und reduziert den Stack.

openFile() implementiert einen Suchpfad OOCPATH für alle Dateien. Damitkönnen Reports, Klassenbeschreibungen und Implementierungen für eine Installati-on oder ein Projekt zentral gespeichert werden.

io.awk enthält noch zwei weitere Funktionen: error() zur Ausgabe einer Fehler-meldung und fatal() zur Ausgabe einer Fehlermeldung und Abbruch des Programmsmit dem Exit-Code 1. error() hinterlegt ebenfalls den Exit-Code 1 als Wert einerglobalen Variablen status. Die Debugger-Module liefern status zum Schluß mit Hilfeeiner END-Aktion.

Wenn main.awk selbst eine END-Aktion enthalten würde, müßte awk nach Be-arbeitung aller BEGIN-Aktionen auf Eingabe warten. Deshalb definieren wir eineawk -Variable debug im Shell-Skript ooc, um festzuhalten, daß wir Debugger-Modu-le mit Musteranweisungen geladen haben. Wenn debug nicht definiert ist, führtdie BEGIN-Aktion in main.awk am Schluß selbst exit aus und setzt den Wert vonstatus als Exit-Code des awk -Programms.

B.3 Erkennung — parse.awkparse.awk extrahiert die Datenbasis aus den Klassenbeschreibungsdateien. Dieäußerste Funktion load(desc) bearbeitet eine ganze Klassenbeschreibungsdateidesc.d. Jede solche Datei wird nur einmal gelesen. classDeclaration(), eine inter-ne Funktion, bearbeitet eine Klassenbeschreibung, structDeclarator() kümmert

Page 227: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

217___________________________________________________________________________B.4 Die Datenbasis

sich um eine Zeile in der Repräsentierung einer Klasse, methodDeclaration() analy-siert die Vereinbarung einer einzigen Methode und declarator() wird für jeden De-klarator aufgerufen.

Die Funktionen selbst sind unkompliziert. Sie verwenden sub() und gsub(), umdie Eingabezeilen zur Erkennung vorzubereiten und split(), um sie in Worte zu zerle-gen. So kann ein allgemeiner C-Deklarator nicht analysiert werden, deshalb be-schränken wir uns auf vereinfachte Deklaratoren, bei denen der Typ dem Namenvorausgeht.

Der Debugger-Modul parse.dbg versteht die Eingaben classes oder descrip-tions und gibt Informationen über alle Klassen oder Klassenbeschreibungsdateienaus oder all für beides zusammen. Für eine Eingabe wie desc.d lädt der Debuggereine Klassenbeschreibungsdatei. Andere Eingaben sollten die Namen von Klassen,Klassenbeschreibungen oder Methoden sein, damit einzelne Einträge aus der Da-tenbasis dargestellt werden.

B.4 Die DatenbasisFür eine Klassenbeschreibungsdatei speichern wir die einzelnen Zeilen, damit wirsie in die Schnittstellen- oder Repräsentierungsdatei kopieren können. Innerhalbder gespeicherten Zeilen müssen wir notieren, wo welche Klassen und Metaklas-sen vereinbart wurden. Diese Information ist auch nötig, um die entsprechendenInitialisierungen zu generieren. Wir erzeugen deshalb die folgenden drei Vektoren:Pub[desc, n] enthält Zeilen für die Schnittstellendatei, Prot[desc, n] enthält Zeilenfür die Repräsentierungsdatei und Dcl[desc, n] notiert nur die Klassen- und Meta-klassen-Vereinbarungen. Für jeden Namen desc einer Klassenbeschreibungsdateispeichern wir mit dem Index 0 die Anzahl der Zeilen und mit den Indizes von 1 auf-wärts die Zeilen selbst. Dcl[desc, 0] existiert genau dann, wenn wir die Klassenbe-schreibungsdatei für desc gelesen haben. Die Zeilen werden unverändert so ge-speichert, wie wir sie von get() bekommen; wir ersetzen aber eine komplette Klas-senvereinbarung durch eine Zeile, die mit % beginnt und den Metaklassen-Namen,falls vereinbart, und dann den Klassennamen enthält.

Für eine Klasse enthält unsere Datenbasis die Namen ihrer Metaklasse undOberklasse, die Komponenten der Repräsentierung und die Namen der Methoden.Wir verwenden insgesamt sechs Vektoren: Meta[class] enthält den Namen derMetaklasse, Super[class] enthält den Namen der Oberklasse, Struct[class, n] isteine Liste der Indizes der Deklaratoren für die Komponenten der Repräsentierungund Static[class, n], Dynamic[class, n], und Class[class, n] enthalten Listen derentsprechend gebundenen Methodennamen. Wieder ist die Länge jeder Liste mitdem Index 0 gespeichert, und die Elemente folgen vom Index 1 an aufwärts.Class[class, 0] existiert genau dann, wenn wir wissen, daß class der Name einerKlasse oder Metaklasse ist.

Für eine Methode müssen wir ihren Namen und Resultattyp sowie die Parame-terliste, Bindung und Markierung für die Methode respondsTo() aufbewahren. Die-se Information repräsentieren wir in den folgenden sechs Vektoren:Method[method] ist der Index des ersten Deklarators; er beschreibt den Metho-

Page 228: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

218___________________________________________________________________________Anhang B Der ‘‘ooc’’-Präprozessor — ‘‘awk’’ Programmiertips

dennamen und den Resultattyp. Die Deklaratoren folgen mit aufsteigenden Indizes;Nparm[method] ist die Anzahl der Parameter. Mindestens der Parameter self mußexistieren. Var[method] ist wahr, wenn method eine variable Anzahl von Parame-tern erlaubt. Linkage[method] ist einer der Werte %, %− oder %+ und bezieht sichauf die Bindung der Methode in Anlehnung an den Präfix in der Klassenbeschrei-bung. Owner[method] ist wichtig für statisch gebundene Methoden; es ist dieKlasse, zu der die Methode gehört, das heißt, die Klasse des Parameters self derMethode. Zuletzt enthält Tag[method] die Voreinstellung der Markierung der Me-thode für respondsTo() und Tag[method, class] die gültige Markierung für methodund class.

Die Komponenten der Repräsentierung einer Klasse und die Methodennamenund Parameter werden als Indizes in eine Liste von Deklaratoren dargestellt. Die Li-ste wird in vier Vektoren gespeichert: Name[index] ist der Name des Deklarators,Const[index] enthält den const-Präfix des Deklarators, falls einer vereinbart wurde,As[index] ist wahr, wenn @ im Deklarator verwendet wurde, das heißt, wenn derDeklarator einen Verweis auf ein Objekt beschreibt. In diesem Fall ist Type[index]entweder ein Klassenname, oder eine leere Zeichenkette, wenn das Objekt zurKlasse der Methode gehört. Wenn As[index] falsch ist, dann ist Type[index] derTyp des Deklarators.

Wenn schließlich die globale Variable lines von Null verschieden ist, enthält dieDatenbasis vier weitere Vektoren: Filename[name] und Fnr[name] geben an, woeine Klasse oder eine Methode vereinbart wurde, und SFilename[name] undSFnr[name] beschreiben, wo die Komponente der Repräsentierung einer Klassevereinbart wurde. Diese Information wird im Modul report.awk verwendet, um#line-Anweisungen zu implementieren.

B.5 Reportgenerierung — report.awkreport.awk enthält Funktionen, um Reports aus Dateien zu laden und Ausgaben ausReports zu erzeugen. Dies ist der einzige Modul, der die Standard-Ausgabeschreibt, deshalb werden in diesem Modul die Zeilennummern verfolgt. Mit einereinfachen Funktion puts() kann eine Zeichenkette und danach ein Zeilentrenner aus-gegeben werden; die print-Anweisung sollte dafür nicht verwendet werden.

Reports werden durch einen Aufruf von loadReports() mit dem Namen der Re-portdatei geladen. Um die Fehlersuche zu vereinfachen, dürfen Reports nicht er-setzt werden.

Ein Aufruf von gen() mit einem Reportnamen interpretiert einen Report und er-zeugt Ausgabe. Hier wird ein gewisser Aufwand getrieben, damit keine führendenLeerzeichen und nicht mehr als eine Leerzeile in Folge ausgegeben werden: Eineglobale Variable newLine ist 0 am linken Rand der Ausgabe und 1, nachdem wir et-was ausgegeben haben; eine interne Funktion lf() gibt einen Zeilentrenner aus undsubtrahiert 1 von newLine. Leerzeichen werden nur ausgegeben, wenn newLineden Wert 1 hat, das heißt, wenn wir uns innerhalb einer Zeile befinden. Zeilentren-ner werden nur ausgegeben, wenn newLine nicht den Wert -1 hat, das heißt, wennwir nicht soeben eine leere Zeile ausgegeben haben.

Page 229: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

219___________________________________________________________________________B.5 Reportgenerierung — ‘‘report.awk’’

Die Reportgenerierung wurde aufgrund einiger einfacher Beobachtungen ent-worfen. Reportzeilen kann man mit geringem Aufwand lesen, mit split() in Wortezerlegen, die durch einzelne Leerzeichen oder Tabulatorzeichen getrennt sind, undin einem großen Vektor Token[] speichern. Ein zweiter Vektor Report[] enthält zujedem Reportnamen den Index seines ersten Worts in Token[]. Die interne FunktionendReport() kontrolliert, daß in jedem Report die geschweiften Klammern balan-ciert, das heißt, die Gruppen korrekt verschachtelt sind, und beendet jeden Reportmit einer zusätzlichen abschließenden Klammer.

Wenn ein einziges Leerzeichen oder ein Tabulatorzeichen zum Trennen beisplit() verwendet wird, und wenn wir ein einziges Leerzeichen für ein leeres Wortausgeben, gleicht ein Report sehr stark der generierten Ausgabe: Zwei Leerzeichenrepräsentieren eines in der Ausgabe. Die Generierung ist recht effizient, wenn wirdie unveränderten Wörter schnell identifizieren (sie beginnen nicht mit einem Ac-cent grave), und wenn wir Ersatztexte schnell finden (sie sind in einem VektorReplace[] abgelegt, dessen Indizes die zu ersetzenden Wörter sind). Die Elementevon Replace[] werden zumeist durch Funktionen im Modul parse.awk eingetragen,die die Datenbasis abfragen: setClass(), setMethod() und setDeclarator() hinterle-gen die Information, die in der Tabelle am Ende der Manualseite von ooc im Ab-schnitt C.1 beschrieben ist.

Gruppen sind leicht zu implementieren. Wenn wir die Reportzeilen lesen, spei-chern wir nach jedem Wort, das mit `{ beginnt, das heißt, am Anfang jeder Gruppe,den Index des Worts, das der zugehörigen schließenden Klammer `} folgt, dasheißt, den Index, der dem Ende der Gruppe folgt. Dazu müssen wir einen Stack deroffenen Klammern verwalten, aber der Stack kann an der gleichen Stelle gespei-chert werden, wo wir später die Indizes der zugehörigen schließenden Klammernspeichern.

Zur Ausführung betreiben wir den Reportgenerator rekursiv. Der Inhalt einerGruppe wird durch einen Aufruf von genGroup() interpretiert, der an der schließen-den Klammer endet. Für eine Schleife rufen wir genGroup() mehrmals auf und set-zen schließlich die Ausführung fort, indem wir dem Index zum Punkt hinter derGruppe folgen. Global ist jeder Report mit der nötigen zusätzlichen schließendenKlammer versehen, damit genGroup() auch von einem ganzen Report zurückkehrt.`{if-Gruppen sind genauso leicht:

`{if • a b group `}

Wenn der Vergleich stimmt, führen wir group rekursiv aus. In jedem Fall setzen wirdie Ausführung hinter der Gruppe fort. Bei `{else haben wir folgende Situation:

`{if • a b group `} `{else • group `}

Wenn der Vergleich stimmt, führen wir seine Gruppe rekursiv aus. Anschließendkönnen wir beiden Indexwerten folgen, oder wir können eine `{else-Gruppe immer

Page 230: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

220___________________________________________________________________________Anhang B Der ‘‘ooc’’-Präprozessor — ‘‘awk’’ Programmiertips

überspringen, wenn sie direkt gefunden wird. Wenn der Vergleich nicht stimmt,und wenn der Index nach `{if auf `{else zeigt, positionieren wir auf `{else und führendie dortige Gruppe rekursiv aus. Anschließend folgen wir dem Index wie üblich.

Das abschließende Wort `} kann nach der Klammer beliebigen Text enthalten.Es gibt aber zwei Spezialfälle. Die Schleife über die Parameter einer Methode ruftgenGroup() mit einem zusätzlichen Parameter more mit Wert 1 auf, wenn nochmehr Methodenparameter bearbeitet werden. Wenn more von Null verschiedenist, gibt genGroup() für `}, als abschließender Klammer ein Komma und ein Leerzei-chen aus. Damit kann man Parameterlisten wesentlich leichter erzeugen.

Für das andere spezielle Abschlußwort `}n wird ein zusätzlicher Zeilentrennerausgegeben, wenn in der Gruppe überhaupt etwas ausgegeben wurde. Die Funkti-on genGroup() liefert ein Resultat, das wahr ist, wenn `}n als Abschlußwort gefun-den wurde. Funktionen wie genLoopMethods(), die eine Schleife über Aufrufe vongenGroup() enthalten, liefern den Resultatwert von genGroup(), wenn die Schleifeüberhaupt ausgeführt wurde, andernfalls liefern sie Null. genGroup() gibt schließ-lich den zusätzlichen Zeilentrenner genau dann aus, wenn die Schleifenfunktionennicht Null liefern, das heißt, wenn die Schleife ausgeführt und mit `}n terminiertwurde. Damit kann man Blöcke im generierten Code schöner trennen.

Der Debugger-Modul report.dbg erhält einen Dateinamen wie c.rep und lädt dieReports aus der Datei. Für einen korrekten Reportnamen wird der Report symbo-lisch dargestellt. Bei der Eingabe all oder reports werden alle Reports ausgegeben.

B.6 ZeilennumerierungEin Präprozessor sollte #line-Anweisungen ausgeben, damit der C-Compiler in sei-nen Fehlermeldungen auf die ursprünglichen Quellzeilen verweisen kann. Leiderkonsultiert ooc verschiedene Eingabedateien, um bestimmte Ausgabezeilen zu er-zeugen, das heißt, es gibt anscheinend keinen impliziten Zusammenhang zwischenKlassenbeschreibungsdateien, Quelldateien und einer Ausgabezeile. Wenn darüberhinaus Reportdateien so formatiert sind, daß sie selbst lesbar sind, tendieren sie da-zu, viele Leerzeilen zu erzeugen, was zu sehr vielen #line-Anweisungen führenkann.

Als Kompromiß generieren wir eine #line-Anweisung nur, wenn ein Report diesverlangt. Die Anweisung kann von einer Klasse, Methode oder Strukturkomponen-te abhängen, oder sie kann die aktuelle Eingabeposition verwenden, die alsFILENAME und FNR vom Modul io.awk verfügbar ist. Die anderen Positionen wur-den von parse.awk notiert. Eine Funktion genLineStamp() in report.awk sammeltdie gewünschte Information und erzeugt die #line-Anweisung.

Wir könnten optimieren und die Ausgabezeilen zählen — die nötige Informationist vollständig in report.awk vorhanden. Die Erfahrung zeigt allerdings, daß dies oocwesentlich verlangsamt. Ein paar zusätzliche #line-Anweisungen oder Zeilentren-ner machen dagegen die C-Übersetzung kaum langsamer.

Insgesamt werden #line-Anweisungen nur erzeugt, wenn die globale Variablelines von Null verschieden ist. Dies ist unter Kontrolle einer Option −l, die an dasShell-Skript ooc übergeben wird.

Page 231: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

221___________________________________________________________________________B.7 Das Hauptprogramm — ‘‘main.awk’’

B.7 Das Hauptprogramm — main.awkDie BEGIN-Aktion in main.awk sollte als letzte ausgeführt werden. Sie bearbeitet je-des Argument in ARGV[] und löscht es aus dem Vektor. Ein Name wie c.rep wirdals Reportdatei interpretiert und mit loadReports() geladen. Ein Name wieObject.dc ist eine Quelle, die vom Präprozessor bearbeitet werden muß. −dc, −hund −r veranlassen, daß gleichnamige Reports interpretiert werden. Jedes andereArgument sollte ein Klassenname sein, für den die Beschreibung mit load() geladenwird; der Name wird als Ersatz für `desc definiert. Ein derartiges Argument mußden meisten anderen Argumenten vorausgehen, denn `desc wird für die Reportge-nerierung aufbewahrt.

load() lädt alle Klassenbeschreibungen aufwärts bis zur Wurzelklasse durch re-kursive Aufrufe. Wenn die awk -Variable makefile durch das Shell-Skript ooc ge-setzt wurde, wird der Report −M für jeden Klassennamen interpretiert, der als Argu-ment angegeben wurde. Dies ergibt normalerweise makefile-Zeilen, die erklären,wie die Klassenbeschreibungsdateien voneinander abhängen. ooc kann jedochnicht erkennen, daß als Resultat des Präprozessors eine Implementierungsdateiclass.c von der Klassenbeschreibungsdatei class.d ebenso wie von der Quelleclass.dc abhängt. Dies muß separat in ein makefile eingetragen werden.

main.awk enthält zwei Funktionen. preprocess() ist der Präprozessor für eineQuelldatei. Zuerst wird der Report include interpretiert, anschließend wirdmethodHeader() für die verschiedenen Zeilen aufgerufen, mit denen ein Metho-denkopf verlangt werden kann, und die Reports casts und init werden für diePräprozessor-Anweisungen %casts und %init interpretiert.

methodHeader() interpretiert den Report methodHeader und notiert die Me-thodendefinitionen in der Datenbasis: Links[class, n] ist die Liste der Methodenna-men, die für class definiert werden, und Tags[method, class] ist die effektive Mar-kierung für method und class. Diese Listen werden im Initialisierungsreport verwen-det.

B.8 ReportdateienReports sind in verschiedenen Dateien gespeichert, um Duplizierung zu vermeidenund die Pflege zu vereinfachen. h.rep und r.rep enthalten die Reports für dieSchnittstellen- und Repräsentierungsdateien. c.rep enthält die Reports für denPräprozessor für eine Quelle. Es gibt zwei Versionen jeder dieser Dateien, eine fürdie Wurzelklasse und eine für alle anderen Klassen. m.rep enthält den Report fürdie makefile-Option −M, und dc.rep enthält den Report für −dc. Drei weitere Datei-en, etc.rep, header.rep und va.rep , enthalten Reports, die von mehr als einer ande-ren Datei aus aufgerufen werden.

Wenn wir die Reports auf mehrere Dateien in Abhängigkeit von Kommandozei-lenoptionen aufteilen, können wir die Kommandozeile im Shell-Skript ooc untersu-chen und nur die Dateien laden, die wirklich benutzt werden. Die Prüfung ist we-sentlich billiger, als viele unbenutzte Reports zu laden und zu durchsuchen.

Page 232: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

222___________________________________________________________________________Anhang B Der ‘‘ooc’’-Präprozessor — ‘‘awk’’ Programmiertips

Mit `{if-Gruppen und Reportaufrufen durch `% kann man mehr oder wenigerverwickelte Lösungen programmieren. Das grundsätzliche Ziel war, die Reportsleichter lesbar und effizienter zu machen. Dazu wurden einige Bedingungen in denReports dupliziert, die vom Report selector aus aufgerufen wurden, und die Re-ports für die Wurzelklasse und die anderen Klassen wurden in verschiedene Dateienaufgeteilt. Einige Reports wurden ohnehin verändert, während ooc in den einzelnenKapiteln entwickelt wurde.

B.9 Das Kommando oocooc kann beliebig viele Reports und Beschreibungen laden, mehrere Schnittstellen-und Repräsentierungsdateien ausgeben und verschiedene Implementierungsdatei-en vorschlagen oder mit dem Präprozessor bearbeiten, alles in Rahmen von einemAufruf. Dies ist eine Konsequenz der modularen Implementierung. ooc ist jedochein reiner Filter, das heißt, verschiedene Dateien werden unter Kontrolle der Kom-mandozeile gelesen, aber die gesamte Ausgabe erfolgt nur als Standard-Ausgabe.Wenn mehrere Ausgaben in einem Aufruf produziert werden, müßten sie mit Hilfevon awk oder csplit bei Bedarf auf verschiedene Dateien verteilt werden. Hier sindeinige typische Aufrufe von ooc:

$ ooc -R Object -h > Object.h # Wurzelklasse$ ooc -R Object -r > Object.r$ ooc -R Object Object.dc > Object.c$ ooc Point -h > Point.h # andere Klasse$ ooc -M Point Circle >> makefile # Abhaengigkeiten$ echo ’Point.c: Point.d’ >> makefile$ ooc Circle -dc > Circle.dc # Implementierung anfangen$ ooc Circle -dc | ooc Circle - > Circle.c # Schwindel...

Wenn ooc ohne Argumente aufgerufen wird, erscheint die folgende Gebrauchsan-weisung:

$ oocusage: ooc [option ...] [report ...] description target ...options: -d arrange for debugging

-l make #line stamps-Dnm=val define val for `nm (one word)-M make dependency for each description-R process root description-7 -8 ... versions for book chapters

report: report.rep load alternative report filedescription: class load class description filetargets: -dc make thunks for last ’class’

-h make interface for last ’class’-r make representation for last ’class’- preprocess stdin for last ’class’source.dc preprocess source for last ’class’

Man muß beachten, daß die explizite Angabe einer einzigen Reportdatei zur Folgehat, daß keiner der Standard-Reports geladen wird. Wenn man nur eine einzige

Page 233: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

223___________________________________________________________________________B.9 Das Kommando ‘‘ooc’’

Standard-Reportdatei ersetzen will, sollte man eine Datei mit gleichem Namenfrüher auf dem OOCPATH anbieten.

Das Shell-Skript ooc muß bei der Installation kontrolliert werden. Es enthältAWKPATH, den Pfad für awk , um die Module zu laden, und OOCPATH, um Reportsund andere Dateien zu finden. Die letztere Variable ist so eingestellt, daß zumSchluß in einem Standard-Katalog gesucht wird; wenn OOCPATH beim Aufruf vonooc bereits definiert ist, wird an die Definition der Standard-Katalog angehängt.

Aus Effizienzgründen prüft das Shell-Skript die ganze Kommandozeile und lädtnur die notwendigen Reportdateien. Wenn ooc nicht korrekt verwendet wird, gibtdas Shell-Skript die Gebrauchsanweisung aus; andernfalls wird awk im gleichen Pro-zeß ausgeführt.

Page 234: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 235: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

225___________________________________________________________________________

Anhang CManual

Dieser Anhang enthält UNIX Manualseiten, die die endgültige Version von ooc und ei-nigen Klassen aus diesem Buch beschreiben.

C.1 Kommandos

munch — Klassenliste erzeugennm −p objekt... archiv... | munch

munch liest die Ausgabe von nm(1) im Berkeley-Format von der Standard-Eingabeund liefert als Standard-Ausgabe eine C-Quelle, die einen null-terminierten Vektorclasses[] definiert, der Zeiger auf die Klassenfunktionen in jedem objekt und archiventhält. Der Vektor ist in Reihenfolge der Funktionsnamen sortiert.

Eine Klassenfunktion ist jeder Name, der mit dem Typ T und, mit einem Unterstrichdavor, auch mit Typ b, d oder s erscheint.

Dies ist eine Hilfskonstruktion, um Programme mit retrieve(2) zu vereinfachen.Daß die Option −p bei Berkeley- und System-V-nm kompatibel ist, ist recht überra-schend.

Da HP/UX-nm static vereinbarte Symbole nicht ausgibt, ist munch bei dieser Platt-form nicht sehr nützlich.

ooc — Präprozessor für objekt-orientierte Codierung mit ANSI Cooc [option ...] [report ...] description target ...

ooc ist ein awk -Programm, das Klassenbeschreibungen liest und die Routinearbei-ten für objekt-orientierte Codierung mit ANSI C übernimmt. Der von ooc generierteCode wird durch Reports beschrieben, die geändert werden können. Diese Manu-alseite beschreibt den Effekt der Standard-Reports.

description ist ein Klassenname. ooc lädt eine Klassenbeschreibungsdatei mit demNamen description.d und rekursiv alle Klassenbeschreibungsdateien für alle Ober-klassen aufwärts bis zur Wurzelklasse. Wenn −h oder −r als target angegeben ist,wird eine C-Definitionsdatei für die öffentliche Schnittstelle oder die private Reprä-sentierung von description als Standard-Ausgabe erzeugt. Wenn source.dc oder −als target angegeben ist, werden #include-Anweisungen für die Definitionsdateienvon description als Standard-Ausgabe erzeugt, und source.dc oder die Standard-Ein-gabe wird gelesen, übersetzt und zur Standard-Ausgabe kopiert. Wenn −dc als tar-get angegeben ist, wird ein Vorschlag für die Implementierung von description mitallen möglichen Methoden als Standard-Ausgabe erzeugt.

Page 236: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

226___________________________________________________________________________Anhang C Manual

Die Ausgabe wird jeweils durch Reportgenerierung aus den Standard-Reportdateienproduziert. Wenn file.rep als report angegeben wird, wird keine Standard-Reportda-tei geladen.

Es gibt einige globale options zur Steuerung von ooc:

−Dname[=value]definiert value oder ein leeres Wort als Ersatz für `name. Dabei sollte nameein einziges Wort sein. ooc definiert GNUC mit dem Wert 0.

−dlädt Debugger-Module, die nach dem normalen Programmablauf ausgeführtwerden. Debugger-Kommandos werden aus der Standard-Eingabe gele-sen: class.d lädt eine Klassenbeschreibungsdatei, report.rep lädt einen Re-port, ein description-, Report-, Klassen- oder Methodenname sorgt für dieAusgabe der entsprechenden Information, und die Befehle all, classes,descriptions oder reports liefern alle Informationen in der jeweiligen Kate-gorie.

−lproduziert #line-Anweisungen nach Maßgabe der Reports.

−Merzeugt makefile -Zeilen, die die Abhängigkeit jeder description von den Da-teien beschreiben, in denen die Oberklassen vereinbart werden.

−Rmuß angegeben werden, wenn die Wurzelklasse bearbeitet wird. In die-sem Fall werden andere Standard-Reports geladen.

EingabeformatAlle Eingabezeilen werden folgendermaßen bearbeitet: Zuerst wird ein Kommentarentfernt, dann werden Zeilen verschmolzen, wenn sie mit einem Gegenschrägstrichenden, als letztes wird Zwischenraum am Schluß entfernt.

Ein Kommentar reicht von // bis zum Ende einer Zeile. Er wird mit dem vorausge-henden Zwischenraum entfernt, bevor Zeilen verschmolzen werden.

Beim Verschmelzen markiert der Gegenschrägstrich den Kontaktpunkt. Der Gegen-schrägstrich und aller Zwischenraum vor und hinter dem Kontaktpunkt werdendurch ein einziges Leerzeichen ersetzt.

ooc verwendet für Namen die gleichen Konventionen wie C, nur dürfen Unterstri-che nicht benutzt werden. Unterstriche dienen intern dazu, Konflikte zwischen oocund anderem Code zu vermeiden.

ooc kann nur vereinfachte C-Deklaratoren verarbeiten. Ein Deklarator darf mit constbeginnen, und die Typinformation muß dem Namen vorausgehen. Die Typinformati-on darf * aber keine Klammern verwenden. Im allgemeinen kann ein beliebiger De-klarator für ooc adaptiert werden, indem man einen Typnamen mit typedef verein-bart.

Eine Zeile, die mit %% beginnt, wirkt als Dateiende.

Page 237: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

227___________________________________________________________________________C.1 Kommandos

KlassenbeschreibungsdateiDie Klassenbeschreibungsdatei hat folgendes Format:

header% meta class {

Komponenten%

Methoden mit statischer Bindung%−

Methoden mit dynamischer Bindung%+

Klassenmethoden%}...

header ist beliebige Information, die zur Standard-Ausgabe kopiert wird, wenn dieSchnittstellendatei erzeugt wird. Information nach %prot wird zur Standard-Ausga-be kopiert, wenn die Repräsentierungsdatei erzeugt wird.

Komponenten sind Deklarationen von C-Strukturkomponenten mit einem Deklaratorpro Zeile. Sie werden in die struct kopiert, die in der Repräsentierungsdatei für dieKlasse erzeugt wird. Sie legen außerdem die Reihenfolge der Parameter für denWurzel-Metaklassen-Konstruktor fest.

Die erste Gruppe von Methoden hat statische Bindung, das heißt, sie sind Funktio-nen mit mindestens einem Objekt als Parameter. Die zweite Gruppe hat dynami-sche Bindung und hat jeweils ein Objekt als Parameter, für das die Methode ausge-wählt wird. Die dritte Gruppe sind Klassenmethoden, das heißt, sie haben jeweilseine Klasse als Parameter, für die die Methode gewählt wird. Das Auswahlobjektheißt immer self. Die Methodendeklarationen definieren C-Funktionsköpfe, Selekto-ren und Information für den Metaklassen-Konstruktor.

Die Kopfzeile der Klassenbeschreibung % meta class { hat eine von drei Formen.Mit der ersten Form wird nur die Wurzelklasse vereinbart:

% meta class {class ist die Wurzelklasse, denn hier ist keine Oberklasse angegeben. AlsOberklasse wird dann die Wurzelklasse selbst definiert, meta sollte an-schließend als Wurzel-Metaklasse vereinbart werden, die daran kenntlichist, daß sie sich selbst als Metaklasse besitzt.

% meta class: super {class ist eine neue Klasse mit der Metaklasse meta und der Oberklassesuper. Diese Form der Kopfzeile wird auch für die Wurzel-Metaklasse ver-wendet, die sich selbst als Metaklasse und die Wurzelklasse als Oberklassehat. Wenn super undefiniert ist, lädt ooc rekursiv (aber jeweils nur einmal)die Klassenbeschreibungsdatei super.d, und dann müssen super und metadefiniert sein, damit class definiert werden kann. Wenn diese Form derKopfzeile verwendet wird, können nur Methoden mit statischer Bindungvereinbart werden.

Page 238: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

228___________________________________________________________________________Anhang C Manual

% meta: supermeta class: super {Diese Kopfzeile vereinbart zusätzlich meta als neue Metaklasse mitsupermeta als ihrer Oberklasse. Wenn super undefiniert ist, lädt ooc rekur-siv (aber jeweils nur einmal) die Klassenbeschreibungsdatei super.d, unddann müssen super und supermeta definiert sein, damit meta und class de-finiert werden können.

Eine Zeile mit einer Methodendeklaration hat folgende Form, wobei geschweifteKlammern null oder mehr Wiederholungen und eckige Klammern optionale Teileeinschließen:

[ Marke : ] Deklarator ( Deklarator { , Deklarator } [ , ... ] );Die optionale Marke ist ein Name, mit dem respondsTo eine Methode fin-den kann. Der erste Deklarator legt den Namen und Resultattyp der Me-thode fest, die weiteren Deklaratoren liefern die Parameternamen und -ty-pen. Genau ein Parametername muß self sein und legt den Empfänger desMethodenaufrufs fest.

Ein Deklarator ist ein vereinfachter C-Deklarator wie oben beschrieben, aber es gibtzwei Spezialfälle:

_namelegt name als Deklarator-Namen fest. Der Typ ist ein Zeiger auf eine In-stanz der aktuellen Klasse oder der Klasse, für die eine dynamisch gebunde-ne Methode ersetzt wird. Für einen derartigen Namen wird von %casts ineiner Methode ein lokaler Strukturzeiger name angelegt. self muß deshalbals _self angegeben werden, wobei self der Zeiger auf die Repräsentierungeines Objekts (oder einer Klasse bei Klassenmethoden) und _self der gene-rische Zeiger ist.

class @ namedefiniert name als Zeiger auf eine Instanz von class. Einen derartigen Zeigerprüft %casts ebenfalls, löst ihn aber nicht auf.

Der Resultattyp einer Methode kann class @ verwenden. In diesem Fall wird derResultattyp als Zeiger auf struct class vereinbart, was bei der Implementierung vonMethoden nützlich ist und nur für Zuweisungen an void * im Anwendungspro-gramm verwendet werden kann. Für Konstruktoren und ähnliche Methoden sollteder Resultattyp void * sein, um die generischen Aspekte der Konstruktoren zu be-tonen.

ÜbersetzungEine Implementierungsdatei source.dc folgt dem oben beschriebenen Eingabefor-mat und wird zur Standard-Ausgabe kopiert. Zeilen, die mit % beginnen, werdenfolgendermaßen übersetzt:

% Klasse Methode {Diese Zeile wird durch einen C-Funktionskopf für die Methode ersetzt; dieFunktion wird static mit dem Namen Klasse_Methode vereinbart, falls dieMethode keine statische Bindung hat. Im letzteren Fall ist die Angabe vonKlasse optional. ooc prüft in allen Fällen, daß die Methode für die Klasse

Page 239: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

229___________________________________________________________________________C.1 Kommandos

definiert werden darf. Die generierten Funktionsnamen werden bei Bedarfaufbewahrt, um die Beschreibung von Klasse und der zugehörigen Meta-klasse zu initialisieren. Vor Klasse kann eine Marke für respondsTo angege-ben werden, falls die Methode keine statische Bindung hat.

%castsDiese Zeile wird durch Definitionen von lokalen Variablen ersetzt, die anStelle von generischen Parameterzeigern auf Objekte in der aktuellen Klas-se zeigen und in bezug auf Klassenzugehörigkeit überprüft werden. Für sta-tisch gebundene Methoden werden anschließend die Parameter geprüft,die auf Objekte aus anderen Klassen zeigen. %casts sollte dort angegebenwerden, wo lokale Variablen definiert werden können; bei statisch gebunde-nen Methoden muß die Anweisung am Schluß der lokalen Vereinbarungenstehen. Nullzeiger bestehen die Prüfung nicht und führen zum Abbruch desProgramms.

%initDiese Zeile sollte am Schluß der Implementierungsdatei stehen. Wenn diedescription eine neue Metaklasse eingeführt hat, werden der Metaklassen-Konstruktor, Selektoren für die neuen dynamisch gebundenen Methodenund die Initialisierungen für die Metaklasse erzeugt. In jedem Fall wird dieInitialisierung der Klasse generiert.

Wenn eine Methode m keine statische Bindung hat, gibt es zwei Selektoren: m mitden gleichen Parametern wie die Methode selbst selektiert die Methode, die fürself zuständig ist, und super_m mit einer explizit angegebenen Klassenbeschrei-bung als zusätzlichem erstem Parameter. Mit super_m wird ein Methodenaufruf andie Oberklasse der Klasse weitergereicht, für die die Methode definiert ist.

Wenn eine dynamisch gebundene Methode oder eine Klassenmethode eine variableParameterliste hat, übergibt der Selektor va_list * app an die eigentliche Methode.

Entdeckt ein Selektor, daß er nicht auf seinen Empfänger angewendet werden darf,ruft er forward auf und übergibt sein Objekt, einen Zeiger auf die Resultatflächeoder einen Nullzeiger, seine eigene Adresse, seinen Namen als Zeichenkette undseine ganze Argumentliste. forward sollte eine dynamisch gebundene Methode inder Wurzelklasse sein, dann kann eine Nachricht von einem Objekt an ein anderesweitergeleitet werden.

MarkenrespondsTo ist eine Methode in der Wurzelklasse, die ein Objekt und eine Markeerhält, das heißt, eine C-Zeichenkette mit einem Namen, und entweder einen Null-zeiger oder einen Selektor liefert, der das Objekt und andere Parameter akzeptiertund die Methode aufruft, die mit der Marke verknüpft ist.

Die Marke, unter der respondsTo eine Klassenmethode oder eine dynamisch ge-bundene Methode findet, wird folgendermaßen festgelegt. Die Voreinstellung istentweder der Methodenname oder die Marke in der Klassenbeschreibungsdatei:

[ Marke : ] Deklarator ( Deklarator { , Deklarator } [ , ... ] );

Page 240: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

230___________________________________________________________________________Anhang C Manual

Im Methodenkopf in der Implementierungsdatei kann die Marke ersetzt werden:% Marke2: Klasse Methode {

Wenn Marke2 angegeben ist, wird sie benutzt, sonst wird Marke verwendet.Wenn Marke2 oder Marke leer ist, aber der Doppelpunkt vorhanden ist, kannrespondsTo die Methode nicht finden.

Reportdateiooc verwendet Reportdateien, die alle Code-Fragmente enthalten, die ooc generiert.Namen wie app für den Zeiger auf eine Argumentliste können in der Reportdateigeändert werden. Nur self ist in ooc selbst eingebaut.

Eine Reportdatei enthält einen oder mehrere Reports. Das früher beschriebene Ein-gabeformat gilt auch hier. Vor jedem Report steht eine Zeile, die mit % beginnt undden Reportnamen enthält, der von Zwischenraum umgeben sein kann. Der Report-name ist beliebiger Text, aber er muß eindeutig sein.

Ein Report besteht aus Zeilen mit Worten, die durch Trenner, nämlich einfache Leer-zeichen oder Tabulatorzeichen, getrennt sind. Ein leeres Wort resultiert zwischenzwei benachbarten Trennern, oder wenn ein Trenner am Anfang oder Ende einerZeile steht.

Für ein leeres Wort wird ein Leerzeichen ausgegeben, allerdings nicht am Anfang ei-ner Ausgabezeile. Dies bedeutet insbesondere, daß zwei aufeinanderfolgendeLeerzeichen im Report ein einfaches Leerzeichen in der Ausgabe repräsentieren.Jedes Wort, das nicht mit einem Accent grave ` beginnt, wird unverändert ausgege-ben.

Ein Wort, das mit `% beginnt, wertet einen anderen Report aus, dessen Name derRest des Worts ist.

Wenn die Option −l angegeben wurde, erzeugt `#line gefolgt von einem weiterenWort eine #line-Anweisung; andernfalls werden die zwei Wörter ignoriert. Wenndas zweite Wort ein Klassen-, Methoden- oder Klassenkomponentenname ist, be-zieht sich die #line-Anweisung auf die Position des Symbols in der Klassenbeschrei-bungsdatei. Andernfalls, und insbesondere für ein leeres zweites Wort, bezieht sichdie #line-Anweisung auf die aktuelle Position in der Eingabedatei.

Ein Wort, das mit `{ anfängt, beginnt eine Gruppe. Die Gruppe endet mit einemWort, das mit `} anfängt. Alle anderen Wörter, die mit einem Accent grave begin-nen, werden bei der Ausgabe ersetzt. Der Ersatz ist manchmal global definiert,oder er wird von bestimmten Gruppen vorgeschrieben. Eine Tabelle aller Ersatz-werte folgt am Ende dieses Abschnitts.

Gruppen sind entweder Schleifen über Teile der Datenbasis, die ooc gesammelthat, oder sie sind Bedingungen, die von einem Vergleich abhängen. Wörter inner-halb einer Gruppe werden unter Kontrolle der Schleife oder der Bedingung ausgege-ben. Anschließend geht die Ausgabe mit dem Wort nach der Gruppe weiter. Grup-pen können verschachtelt werden, aber das ist für manche Teile der Datenbasissinnlos. Hier ist eine Tabelle der Wörter, die eine Schleife beginnen:

Page 241: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

231___________________________________________________________________________C.1 Kommandos

`{% statisch gebundene Methoden der aktuellen `class`{%− dynamisch gebundene Methoden der aktuellen `class`{%+ Klassenmethoden der aktuellen `class`{() Parameter der aktuellen `method`{dcl Klassenkopfzeilen der Beschreibungsdatei `desc`{pub öffentliche Zeilen der Beschreibungsdatei `desc`{prot Zeilen nach %prot in der Beschreibungsdatei `desc`{links class ersetzte dynamische und Klassenmethoden für class`{struct class Komponenten für class`{super `desc und alle Oberklassen zurück zu `root

Eine Schleife endet mit einem Wort, das mit `} beginnt. Wenn das abschließendeWort in der Schleife über Parameter `}, ist, und wenn die Schleife noch nicht been-det ist, wird für dieses Wort ein Komma und ein Leerzeichen ausgegeben. Wenndas abschließende Wort einer Gruppe `}n ist, und wenn die Gruppe irgendeine Aus-gabe produziert hat, wird ein Zeilentrenner für dieses Wort ausgegeben. Andern-falls wird am Schluß einer Gruppe nichts ausgegeben.

Eine Bedingungsgruppe fängt mit `{if oder `{ifnot an. Die nächsten zwei Wörter bil-den die Bedingung: Sie werden ersetzt, wenn sie mit Accent grave anfangen, unddann verglichen. Wenn die Texte gleich sind, wird die `{if-Gruppe ausgeführt, wennnicht, wird die `{ifnot-Gruppe ausgeführt. Wenn eine der beiden Gruppen nicht aus-geführt wird, und wenn ihr unmittelbar eine Gruppe folgt, die mit `{else beginnt,wird diese Gruppe ausgeführt; andernfalls wird die `{else-Gruppe übergangen.

Im allgemeinen sollte `{else dem Wort `} am Schluß einer `{if-Gruppe auf der glei-chen Reportzeile nach einem einzigen Trenner unmittelbar folgen.

Hier ist eine Tabelle der Ersetzungen zusammen mit dem Kontext, von dem sie je-weils abhängen:

global definiert` kein Text (leere Zeichenkette)`` ` (Accent grave)`t Tabulatorzeichen`n Zeilentrenner (maximal eine Leerzeile)

definiert, wenn Klassenbeschreibungen geladen sind`desc letzte description der Kommandozeile`root Name der Wurzelklasse`metaroot Name der Meta-Wurzelklasse

definiert für Klasse % %− %+ `{dcl `{prot `{pub `{super`class Klasse`super Oberklasse`meta Metaklasse`supermeta Oberklasse der Metaklasse

Page 242: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

232___________________________________________________________________________Anhang C Manual

definiert für Methode `{% `{%− `{%+ `{links class`method Name`result Resultattyp`linkage Bindung: %, %−, oder %+`tag Markierung für respondsTo`,... , ... bei variabler Parameterliste, sonst leer`_last letzter Parameter, falls variable Liste, sonst undefiniert

definiert für Deklarator `{() `{struct class`name Name`const const und Leerzeichen, falls vereinbart`type void * für Objekte, sonst vereinbarter Typ`_ _ wenn in Deklaration, sonst leer`cast Klassenname für Objekt, sonst leer

definiert für Zeilen aus Beschreibung `{dcl `{prot `{pub`class definiert für Klassenbeschreibung, sonst leer`line Zeile außerhalb von Klassenbeschreibung, sonst leer`newmeta 1 falls neue Metaklasse vereinbart, 0 falls nicht

Eine description auf der Kommandozeile von ooc definiert den Ersatz für eine Klas-se. Wenn ein Methodenkopf in der Implementierungsdatei verlangt wird, wird derErsatz für eine Klasse und eine Methode definiert. Die Schleifen `{dcl, `{prot und`{pub definieren jeweils den Ersatz für eine Zeile aus einer Klassenbeschreibungsda-tei. Die Schleifen `{%, `{%−, `{%+ und `{links class definieren jeweils den Ersatz füreine Methode. Die Schleife `{() definiert jeweils den Ersatz für einen Parameter-De-klarator. Die Schleife `{struct class definiert jeweils den Ersatz für einen Deklaratoreiner Klassenkomponente. Die Schleife `{super läuft von der description aufwärtsdurch alle Oberklassen.

EnvironmentOOCPATH ist eine Liste von Pfaden, die durch Doppelpunkt getrennt sind. Wenn einDateiname keine Schrägstriche als Pfadtrenner enthält, sucht ooc nach der Datei,indem jeder Pfad aus OOCPATH vor den gesuchten Dateinamen gesetzt wird. Diesgeschieht für Klassenbeschreibungen, Quellen und Reportdateien. Nach Voreinstel-lung enthält OOCPATH den aktuellen Katalog und einen Standard-Katalog.

Da ooc mit awk realisiert ist, können die Module des awk -Programms mit der Envi-ronment-Variablen AWKPATH gesucht werden.

DATEIEN class.d Beschreibung für classclass.dc Implementierung für classreport.rep ReportdateiAWKPATH/ *.awk ModuleAWKPATH/ *.dbg Debugger-ModuleOOCPATH/ c.rep Reports für ImplementierungOOCPATH/ dc.rep Report für −dcOOCPATH/ etc.rep gemeinsame ReportsOOCPATH/ h.rep Report für Schnittstelle

Page 243: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

233___________________________________________________________________________C.2 Funktionen

OOCPATH/ header.rep gemeinsame ReportsOOCPATH/ m.rep Report für −MOOCPATH/ r.rep Reports für RepräsentierungOOCPATH/ va.rep gemeinsame ReportsOOCPATH/ [chr]-R.rep Reportversion für Wurzelklasse

Der C-Präprozessor wird auf die Ausgabe von ooc angewendet, nicht auf die Einga-be, das heißt, bedingte Übersetzung sollte nicht auf die ooc-Anweisungen ange-wendet werden.

C.2 Funktionen

retrieve — Objekt aus Eingabestrom ladenvoid * retrieve (FILE * fp)

retrieve liefert ein Objekt, das aus dem Eingabestrom eingelesen wird, den fp re-präsentiert. Am Dateiende oder bei einem Fehler liefert retrieve einen Nullzeiger.

retrieve benötigt eine sortierte Tabelle von Zeigern auf Klassenfunktionen, die zumBeispiel mit munch(1) erzeugt werden kann. Wenn die richtige Klassenbeschrei-bung gefunden ist, wendet retrieve die Methode geto auf eine Fläche an, die vonallocate stammt.

SIEHE AUCH munch(1), Object(3)

C.3 Wurzelklassen

intro — Einführung zu den WurzelklassenObject Class

Exception

Object(3) ist die Wurzelklasse. Class(3) ist die Wurzel-Metaklasse. Die meistenMethoden, die für Object vereinbart sind, werden in den Standard-Reports vonooc(1) verwendet, das heißt, sie können nicht verändert werden, ohne daß auch dieReports verändert werden.

Exception(3) verwaltet einen Stack mit verschachtelten Fehlerbehandlungen. Die-se Klasse wird im Zusammenhang mit ooc nicht unbedingt benötigt.

Class Class: Object — Wurzel-MetaklasseObject

Class

new(Class(), name, superclass, size, selector, tag, method, ... , 0);

Object @ allocate (const self)const Class @ super (const self)const char * nameOf (const self)

Page 244: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

234___________________________________________________________________________Anhang C Manual

Ein Metaklassenobjekt beschreibt eine Klasse, das heißt, es enthält den Klassenna-men name, einen Zeiger super auf die Beschreibung der Oberklasse, die Größe sizeeines Objekts der Klasse und Information über alle dynamisch gebundenen Metho-den, die auf ein Objekt der Klasse angewendet werden können. Diese Informationbesteht aus einem Zeiger selector auf die Selektor-Funktion, einer Zeichenkette tagfür die Methode respondsTo, die leer sein kann, und einen Zeiger method auf dieeigentliche Funktion für Objekte der Klasse.

Eine Metaklasse ist eine Sammlung von Metaklassenobjekten, die alle die gleichenMethodeninformationen enthalten, wobei natürlich jedes Metaklassenobjekt auf an-dere Methoden zeigen kann. Eine Metaklassen-Beschreibung beschreibt eine Me-taklasse.

Class ist die Wurzel-Metaklasse. Es gibt ein Metaklassenobjekt Class, das die Me-taklasse Class beschreibt. Jede andere Metaklasse X wird durch ein anderes Meta-klassenobjekt X beschrieben, das zu Class gehört.

Die Metaklasse Class enthält ein Metaklassenobjekt Object, das die WurzelklasseObject beschreibt. Eine neue Klasse Y, die die gleichen dynamisch gebundenenMethoden wie die Klasse Object hat, wird durch ein Metaklassenobjekt Y beschrie-ben, das zu Class gehört.

Eine neue Klasse Z, die mehr dynamisch gebundene Methoden hat als Object,benötigt ein Metaklassenobjekt Z, das zu einer neuen Metaklasse M gehört. Dieseneue Metaklasse hat eine Metaklassen-Beschreibung M, die zu Class gehört.

Mit dem Konstruktor von Class werden neue Klassenbeschreibungsobjekte wie Yund Metaklassen-Beschreibungsobjekte wie M erzeugt. Mit dem Konstructor vonM werden neue Klassenbeschreibungsobjekte wie Z erzeugt. Der Konstruktor vonY erzeugt Objekte, die zur Klasse Y gehören, und der Konstruktor von Z erzeugt Ob-jekte der Klasse Z.

allocate reserviert Speicher für ein Objekt in der Klasse, deren Beschreibung als Ar-gument übergeben wird, und verknüpft die Speicherfläche mit dieser Klassenbe-schreibung. Wenn new nicht ersetzt wird, ruft diese Methode allocate auf undwendet ctor auf das Resultat an. retrieve ruft allocate auf und wendet geto aufdas Resultat an.

super liefert die Oberklasse aus einer Klassenbeschreibung.

nameOf liefert den Namen aus einer Klassenbeschreibung.

Der Konstruktor ctor von Class sorgt für Vererbung von Methoden. An new müs-sen nur Informationen für die ersetzten Methoden übergeben werden. Die Informa-tion besteht aus der Adresse des Selektors, mit dem die Methode ausgewählt wird,einer Zeichenkette als Markierung für respondsTo, die leer sein kann, und derAdresse der neuen Methode. Verschiedene Methoden können in beliebiger Reihen-folge angegeben werden; Null an Stelle eines neuen selector-Werts beendet die Li-ste.

delete, dtor und geto sind ohne Funktion für Klassenbeschreibungen.

Auf Klassenbeschreibungen wird nur über Funktionen zugegriffen, die die Beschrei-bung beim ersten Aufruf initialisieren.

SIEHE AUCH ooc(1), retrieve(2)

Page 245: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

235___________________________________________________________________________C.3 Wurzelklassen

Class Exception: Object — verschachtelte Fehlerbehandlungenverwalten

ObjectException

new(Exception());

int catch (self)void cause (int number)

Exception ist eine Klasse, mit der ein Stack von Fehlerbehandlungen verwaltetwird. Nach Vorbereitung durch einen Aufruf von catch kann das neuste Exception-Objekt eine von Null verschiedene Fehlernummer empfangen, die mit cause ver-schickt wird.

ctor legt das neue Exception-Objekt auf dem globalen Stack aller derartiger Objek-te ab, dtor entfernt es. Diese Aufrufe müssen symmetrisch erfolgen.

catch bereitet sein Objekt auf den Empfang einer Fehlernummer vor. Wenn dieNummer geschickt wird, liefert sie catch. Diese Funktion ist als Makro mit Hilfe vonsetjmp(3) implementiert und unterliegt den gleichen Einschränkungen, insbesonde-re muß die Funktion, die den Aufruf von catch enthält, noch immer aktiv sein, wenndie Fehlernummer geschickt wird.

Andere Methoden sollten auf ein Exception-Objekt nicht angewendet werden.

SIEHE AUCH setjmp(3)

Class Object — WurzelklasseObject

Class

new(Object());

typedef void (* Method) ();

const void * classOf (const self)size_t sizeOf (const self)int isA (const self, const Class @ class)int isOf (const self, const Class @ class)void * cast (const Class @ class, const self)Method respondsTo (const self, const char * tag)%−void * ctor (self, va_list * app)void delete (self)void * dtor (self)int puto (const self, FILE * fp)void * geto (self, FILE * fp)void forward (self, void * result, Method selector, const char * name, ...)%+Object @ new (const self, ...)

Page 246: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

236___________________________________________________________________________Anhang C Manual

Object ist die Wurzelklasse, also die endgültige Oberklasse aller Klassen und Meta-klassen. Metaklassen haben Class als ihre vorletzte Oberklasse.

classOf liefert die Klassenbeschreibung eines Objekts; sizeOf liefert die Größe ei-nes Objekts in Bytes.

isA ist wahr, wenn ein Objekt auf eine bestimmte Klassenbeschreibung verweist,das heißt, wenn das Objekt zu der angegebenen Klasse gehört. isA ist falsch fürNullzeiger. isOf ist wahr, wenn ein Objekt zu einer bestimmten Klasse gehört odersie schließlich als Oberklasse hat. isOf ist falsch für Nullzeiger und wahr für beliebi-ge Objekte und die Klasse Object.cast überprüft, ob sein zweites Argument direkt oder schließlich vom ersten Argu-ment beschrieben wird. Wenn nicht, und insbesondere für Nullzeiger, wird der auf-rufende Prozeß abgebrochen. cast liefert normalerweise sein zweites Argumentund könnte aus Effizienzgründen durch einen trivialen Makro ersetzt werden.

respondsTo liefert Null oder einen Selektor, der zu der Markierung für ein Objektgehört. Wenn das Resultat nicht Null ist, kann das Objekt mit den nötigen weiterenArgumenten an diesen Selektor übergeben werden.

ctor ist der Konstruktor. Diese Methode erhält die restlichen Argumente von new.Sie sollte zuerst die Methode super_ctor aufrufen, die vielleicht einen Teil der Argu-mentliste verwendet; anschließend sollte die eigene Intialisierung mit dem Rest derArgumentliste erfolgen.

Wenn die Methode nicht ersetzt wird, zerstört delete ein Objekt durch Aufruf vondtor und Übergabe des Resultats an free(3). Nullzeiger können nicht an deleteübergeben werden, da delete dynamisch gebunden ist.

dtor muß die Ressourcen freigeben, die ein Objekt eingeworben hat. Die Methoderuft normalerweise zum Schluß super_dtor auf und läßt diese Methode das Resul-tat festlegen. Wenn ein Nullzeiger als Resultat geliefert wird, gibt delete dann denSpeicherplatz für das Objekt nicht frei.

puto gibt eine ASCII-Repräsentierung eines Objekts in einen Strom aus. Die Metho-de ruft normalerweise super_puto auf, so daß die Ausgabe mit dem Klassennamenbeginnt. Die Repräsentierung muß so entworfen werden, daß geto alles mit Aus-nahme des Klassennamens aus dem Strom lesen und die Information in die Flächeeintragen kann, die als erstes Argument übergeben wird. Die Methode geto funk-tioniert fast wie ctor und ruft normalerweise super_geto auf, damit die Oberklas-sen ihre Anteile eines Objekts zuerst einlesen, die die zugehörigen puto-Methodengeschrieben haben.

forward wird von einem Selektor aufgerufen, der feststellt, daß er nicht auf das ge-wünschte Objekt angewendet werden kann. Die Methode kann ersetzt werden,um Nachrichten weiterzuleiten.

Wenn new nicht ersetzt wird, ruft diese Methode allocate auf und übergibt das Re-sultat, zusammen mit seinen restlichen Argumenten, an ctor.SIEHE AUCH ooc(1), retrieve(2), Class(3)

Page 247: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

237___________________________________________________________________________C.4 Klassen im GUI-Rechner

C.4 Klassen im GUI-Rechner

intro — Einführung zum GUI-RechnerObjct Class

EventIc IcClass

ButtonCalcCrt

CButtonCLineOut

LineOutMux

List ListClassXt

XawBoxXawCommand

XButtonXawFormXawLabel

XLineOutXtApplicationShell

Object(3) ist die Wurzelklasse. Object muß in Objct umbenannt werden, da der ur-sprüngliche Name von X11 verwendet wird.

Event(4) ist eine Klasse, mit der Eingabedaten wie ein Tastendruck oder ein Maus-Klick repräsentiert werden.

Ic(4) ist die Basisklasse zur Repräsentierung von Objekten, die Event-Objekte emp-fangen, verarbeiten und versenden können. Button verwandelt Eingaben in Ausga-ben mit vordefinierten Textwerten. Calc verarbeitet Texte und sendet Resultateweiter. LineOut zeigt einen Eingabetext an. Mux versucht, eine Eingabe an einesvon mehreren Objekten weiterzuleiten.

Crt(4) ist eine Klasse, die eine Verbindung zur curses-Bibliothek für Bildschirmdialo-ge aufbaut. Ein Crt-Objekt sendet Positionierungsdaten, wenn der Cursor bewegtund mit return bestätigt wird, und Texte, wenn andere Tasten gedrückt werden.CButton implementiert Button auf einem von curses verwalteten Bildschirm.CLineOut implementiert LineOut.List verwaltet eine Liste von Objekten, wie im siebten Kapitel erläutert.

Xt(4) ist eine Klasse, die eine Verbindung zum X-Toolkit aufbaut. Die Unterklassenverbergen Toolkit- und Athena-Widgets. XButton implementiert einen Button miteinem Command-Widget. XLineOut implementiert LineOut mit einem Label-Widget.

SIEHE AUCH curses(3), X(1)

Page 248: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

238___________________________________________________________________________Anhang C Manual

IcClass Crt: Ic — Objekte für cursesObjct

IcCrt

CButtonCLineout

new(Crt());new(CButton(), "text", y, x);new(CLineOut(), y, x, len);

void makeWindow (self, int rows, int cols, int x, int y)void addStr (self, int y, int x, const char * s)void crtBox (self)

Ein Crt-Objekt verbirgt ein mit curses(3) realisiertes Window. Die curses-Bibliothekwird initialisiert, wenn das erste Crt-Objekt erzeugt wird.

Crt_gate ist die Hauptschleife: Diese Methode überwacht die Tastatur. Sie imple-mentiert eine Cursor-Bewegung im Stil von vi(1) mit den Tasten hjkl und vielleichtauch den Pfeiltasten. Wenn return gedrückt wird, wird ein Event-Objekt mit kind 1und einem Vektor mit Spalten- und Zeilenposition verschickt. Wenn control-D ge-drückt wird, endet Crt_gate mit reject. Jeder andere Tastendruck wird als Event-Objekt mit einer Zeichenkette verschickt, die aus dem eingegebenen Zeichen be-steht.

Ein CLineOut-Objekt implementiert ein LineOut-Objekt auf einem von curses ver-walteten Bildschirm. Die ankommenden Zeichenketten sollten nicht mehr als lenBytes enthalten.

Ein CButton-Objekt implementiert ein Button-Objekt auf einem von curses verwal-teten Bildschirm. Wenn das Objekt einen passenden Text erhält, wird er weiterver-schickt. Wenn Positionierungsdaten, zum Beispiel von einem Crt-Objekt, eintreffenund die Koordinaten im Window des CButton-Objekts liegen, wird der eigene Textweiterverschickt.

SIEHE AUCH Event(4)

Class Event: Objct — EingabedatenObjct

Event

new(Event(), kind, data);

int kind (self)void * data (self)

Ein Event-Objekt repräsentiert Eingabedaten wie einen Tastendruck, einen Maus-Klick etc.

Page 249: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

239___________________________________________________________________________C.4 Klassen im GUI-Rechner

kind ist Null, wenn data eine statische Zeichenkette ist. kind ist nicht Null, wenndata ein Zeiger ist. Insbesondere kann ein Maus-Klick mit kind 1 und einem Zeigerals data repräsentiert werden, der auf einen Vektor mit zwei ganzzahligen Koordina-ten zeigt.

SIEHE AUCH Ic(4)

IcClass: Class Ic: Objct — einfache EreignisverarbeitungObjct

IcButtonCalcLineOutMux

new(Ic());new(Button(), "text");new(Calc());new(LineOut());new(Mux());

%−void wire (Objct @ to, self)enum { reject, accept } gate (self, const void * item)

Ein Ic-Objekt hat eine Ausgabeleitung und eine Eingabeaktion. wire verbindet dieAusgabeleitung mit einem anderen Objekt. Wenn ein Ic-Objekt Eingabedaten alsitem mit der Methode gate erhält, wird es normalerweise eine Aktion durchführenund ein Resultat über seine Ausgabeleitung verschicken. Manche Ic-Objekte erzeu-gen nur Ausgaben und andere verarbeiten nur Eingaben. gate liefert accept, wennder Empfänger die Daten akzeptiert.

Ic ist eine Basisklasse. Die Unterklassen ersetzen gate, um ihre eigenen Aktionenzu implementieren. Ic_gate erhält item und benützt gate, um den Wert über seineAusgabeleitung zu verschicken, das heißt, eine Unterklasse wird super_gate ver-wenden, um selbst etwas zu ihrer Ausgabeleitung zu schicken.

Ein Button-Objekt enthält einen Text, der als Antwort auf bestimmte Eingaben ver-schickt wird, bei denen es sich um Event-Objekte handeln muß. Wenn ein Event-Objekt den gleichen Text oder einen Nullzeiger oder andere Daten enthält, akzep-tiert das Button-Objekt die Eingabe und schickt seinen eigenen Text weiter. Ein an-derer Text wird abgelehnt.

Button ist als Basisklasse entworfen. Unterklassen sollten Mauspositionen etc. un-tersuchen und mit super_gate den entsprechenden Text verschicken lassen.

Ein Calc-Objekt erhält eine Zeichenkette, berechnet ein Resultat und schickt den ak-tuellen Wert als Zeichenkette weiter. Nur das erste Zeichen der Eingabe wird verar-beitet: Ziffern werden zu einer nicht-negativen Zahl zusammengesetzt; +, −, * und/ führen arithmetische Operationen mit zwei Operanden aus; = beendet eine arith-

Page 250: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

240___________________________________________________________________________Anhang C Manual

metische Operation; C löscht alle Informationen und Q beendet die Anwendung.Das Calc-Objekt ist eine vorrangfreie endliche Maschine: Die erste Ziffernkette de-finiert einen ersten Operanden; der erste Operator wird gespeichert; weitere Zifferndefinieren einen weiteren Operanden; wenn ein weiterer Operator empfangen wird,wird der gespeicherte Operator ausgeführt und der neue Operator gespeichert. Un-zulässige Eingaben werden akzeptiert und stillschweigend ignoriert.

Ein LineOut-Objekt akzeptiert eine Zeichenkette und zeigt sie.

Ein Mux-Objekt kann mit einer Liste von anderen Objekten verbunden werden. Essendet seine Eingabe der Reihe nach zu jedem Objekt, bis eines die Eingabe akzep-tiert. Die Liste wird in der Reihenfolge der Aufrufe von wire aufgebaut und durch-sucht.

SIEHE AUCH Crt(4), Event(4), Xt(4)

Class Xt: Object — Objekte für X11Objct

XtXawBoxXawCommand

XButtonXawFormXawLabel

XLineOutXtApplicationShell

new(Xt());new(XtApplicationShell(), & argc, argv);new(XawBox(), parent, "name");new(XawCommand(), parent, "name");new(XawForm(), parent, "name");new(XawLabel(), parent, "name");new(XButton(), parent, "name", "label");new(XLineOut(), parent, "name", "label");

void * makeWidget (self, WidgetClass wc, va_list * app)void addAllAccelerators (self)void setLabel (self, const char * label)void addCallback (self, XtCallbackProc fun, XtPointer data)

void mainLoop (self)Ein Xt-Objekt verbirgt ein Widget aus dem X-Toolkit. makeWidget erzeugt dasWidget und installiert es in der Hierarchie einer Anwendung. Aus der Argumentli-ste, auf die app zeigt, erhält die Methode ein Xt-Objekt als parent und einen Namenfür das Widget. Mit addAllAccelerators werden die Akzeleratoren in der Hierar-chie unter dem Xt-Objekt installiert. setLabel definiert eine label-Ressource. add-Callback fügt eine Callback-Funktion zur callback-Liste hinzu.

Page 251: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

241___________________________________________________________________________C.4 Klassen im GUI-Rechner

Ein XtApplicationShell-Objekt verbirgt ein ApplicationShell-Widget aus dem X-Tool-kit. Wenn das Objekt erzeugt wird, wird auch das Shell-Widget erzeugt, und die X-Toolkit-Optionen werden aus den Kommandoargumenten entfernt, die an newübergeben wurden. Die Hauptschleife der Anwendung ist mainLoop.

XawBox, XawCommand, XawForm und XawLabel verbergen die entsprechen-den Athena-Widgets. Wenn die Objekte erzeugt werden, werden die Widgetsebenfalls erzeugt. setLabel ist sinnvoll für XawCommand und XawLabel. EineCallback-Funktion kann für ein XawCommand-Objekt mit addCallback eingetragenwerden.

Ein XButton-Objekt ist ein Button-Objekt, das mit einem XawCommand-Objektimplementiert wird. Es leitet wire an sein internes Button-Objekt weiter, und es in-stalliert einen Callback zu gate für dieses interne Button-Objekt, so daß sein textverschickt wird, wenn die Aktion notify ausgelöst wird, das heißt, wenn der Knopfgeklickt wird. Akzeleratoren können dazu benutzt werden, notify durch Tasten-druck auszulösen.

Ein XLineOut-Objekt ist ein LineOut-Objekt, das mit einem XawLabel-Objekt im-plementiert ist. Es schickt gate an sich selbst weiter, um eine Zeichenkette zuempfangen und darzustellen. Wenn das übergeordnete Widget dies erlaubt, ändertdas Label-Widget seine Größe in Abhängigkeit vom empfangenen Text.

SIEHE AUCH Event(4)

Page 252: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 253: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

243___________________________________________________________________________

Literaturverzeichnis

[ANSI] American National Standard for Information Systems — ProgrammingLanguage C X3.159-1989.

[AWK88] A. V. Aho, B. W. Kernighan und P. J. Weinberger The awk ProgrammingLanguage Addison-Wesley 1988, ISBN 0-201-07981-X.

[Bud91] T. Budd An Introduction to Object-Oriented Programming PrenticeHall 1991, ISBN 0-201-54709-0.

[Ker82] B. W. Kernighan ‘‘pic — A Language for Typesetting Graphics’’ Software— Practice and Experience Januar 1982.

[K&P86] B. W. Kernighan and R. Pike Der UNIX-Werkzeugkasten Hanser 1986,ISBN 3-446-14273-8.

[K&R83] B. W. Kernighan and D. M. Ritchie Programmieren in C Hanser 1983,ISBN 3-446-13878-1.

[K&R89] B. W. Kernighan and D. M. Ritchie Programmieren in C Zweite Ausgabe,Hanser 1989, ISBN 3-446-15497-3.

[Sch87] A. T. Schreiner UNIX Sprechstunde Hanser 1987, ISBN 3-446-14894-9.

[Sch89] A. T. Schreiner C-Praxis mit curses, lex, und yacc Hanser 1989, ISBN 3-446-15391-8.

Page 254: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen
Page 255: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

245___________________________________________________________________________

Sachverzeichnis

#line 92, 220% 80, 227f%% 226%+ 142, 227%- 80, 227%casts 86, 103f, 106, 109, 229%init 229%prot 81// 80, 226@ 84, 228‘ 88f, 231‘#line 230‘% 88, 230‘,... 232‘cast 88, 232‘class 231f‘const 88, 232‘desc 231‘line 232‘linkage 88, 232‘meta 231‘metaroot 90, 231‘method 88, 232‘n 89, 231‘name 88, 232‘newmeta 88, 232‘result 88, 232‘root 90, 231‘super 231‘supermeta 231‘t 89, 231‘tag 232‘type 88, 232‘_ 88, 232‘_last 232‘‘ 231‘{ 88, 230‘{% 231‘{%+ 142, 231‘{%- 88, 231‘{() 88, 231‘{dcl 231‘{else 231

‘{if 88, 231‘{ifnot 231‘{links 231‘{prot 231‘{pub 231‘{struct 231‘{super 231‘} 88, 230‘}, 88, 231‘}n 88, 231

Aabstrakte Basisklasse 122, 150abstrakter Datentyp 1f, 9f, 13Abstraktion 21accelerator 198, 203action function 198Add 26Aggregat und Vererbung 45allocate() 142, 153

Implementierung 143ANSI-C 207ffAny 65fany, testet Any 65Anweisungen, ooc 86ApplicationShell 197fArgumente prüfen 76Argumentliste, variable 14, 200, 213

aufteilen 15, 42, 69arithmetischer Ausdruck 23ff, 49ff

Bewertung 30infix, ausgeben 23, 31Klammern 27Klassen 143ffpostfix, ausgeben 23, 29Repräsentierung 26ffvalue, bewerten 23, 30, 49ff, 137ff,

152, 155, 165ff, 175fassert() 5f, 212Assign 51, 56Athena Widgets, Xaw 197Atom 19

Page 256: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

246___________________________________________________________________________Sachverzeichnis

atoms, testet Atom 20awk 215AWKPATH 223, 232

BBag 7ffbags, testet Bag 9Basisklasse 36ff

abstrakt 122, 150Bedingung, ooc 231Bewertung, arithmetischer Ausdruck 30Bindung

dynamisch 15, 20, 23, 27, 61f, 152entwerfen 39Konstruktor 44spät siehe dynamischstatisch und dynamisch 38fVererbung 37, 44

Box 198Button 186, 191, 239button, testet Button 191f

CCalc 186, 192, 239fCallback-Funktion 121, 135, 198, 201cast(), dynamische Typprüfung 100ff,

107ffcasts, Report 88, 103catch() , Fehlerbehandlung vereinbaren

175cause() , Fehlerbehandlung auslösen

172ffCButton 194f, 238cbutton, testet Crt 196checks, Report 89, 103Circle 35ff, 39ff, 43, 75, 80, 99circles, testet Circle 41f, 75Class 64, 75, 90, 142, 233f

Implementierung 67Konstruktor 68f

Class-Responsibility-Collaborator 188classOf, Report 105, 185classOf() , Klasse bestimmen 67, 99CLineOut 194, 238

Code-Duplikation 92Codierstandards 79, 91

Typprüfung 102ffCommand 198const 209

Funktionsresultat 116, 209fooc 84fParameter 210typedef 210

const, Report 117Container-Klasse 161

speichern 169Crt 194, 238crun, Taschenrechner mit curses 196ctor(), Konstruktor 12, 14f, 56, 62f

Argumente teilen 15curses 193

DDateiname, Klassenbeschreibung 82, 85Datentyp 1f

abstrakt 1f, 9f, 13defensive Programmierung 79Definitionsdatei 2, 9

einmal einfügen 2Unterklasse 73

Deklarator, ooc 84, 226, 228Delegate 123f, 135

einrichten 129delete() , Speicherverwaltung 3ff, 10ff,

15, 70, 143Destruktor, dtor() 11, 15, 62f

Unterklasse 42, 44, 76Dialogsprache, Klassenhierarchie 168Div 27down-cast 37dtor(), Destruktor 11, 15, 62fdynamische Bindung 15, 20, 23, 27,

61f, 99, 152new() 139Konstruktor 44Vererbung 37Vergleich, statische Bindung 38f

dynamische Typprüfung 100ffdynamischer Speicher 18

Page 257: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

247___________________________________________________________________________Sachverzeichnis

Eeinfache Vererbung 36Eingabeformat, ooc 226ffEmpfänger, self 13ffendliche Maschine 192Entwurf

objekt-orientiert 185f, 188, 193f,198f

ooc 79, 84ff, 91Ersetzung, ooc 231erzeugen

Klasse 63Namen 76Unterklasse 65

Event 186, 190f, 238fexcept, testet Exception 177Exception 173ff, 178, 235extern und static 117

FFehlerbehandlung 5f, 24, 66, 76, 171ff,

178auslösen, cause() 172ffnumerische Fehler 59vereinbaren, catch() 175

Filter 85fFilter 123ffForm 198, 203forward() , Nachrichten weiterleiten

181ff, 200, 202, 229Klassenmethoden 184

forward, Report 183Framework 125, 135free() 3, 214friend 174Funktion

Callback 121, 135, 198, 201const Resultat 116, 209ffriend 174generisch 16Klassenbeschreibung 115Makro 40, 45mathematisch 57polymorph 15f, 21, 44Prototyp 207

Funktion ←Typ prüfen 128ftypspezifisch 20überladen 16void * 116Zeiger 11, 211

GGeltungsbereich 207Generierung, ooc 87ffgenerische Funktion 16generischer Zeiger, void * 3, 208geto(), Objekte füllen 162ff, 169

Konstruktor 163puto() 163f

Grammatik 25, 50

Hhello, testet LineOut 190

IIc 186ff, 239ifmethod, Report 105Implementierung, ooc 87, 215ffImplementierungsdatei 82ff, 86, 228finfix, Ausdruck ausgeben 23, 31Information verbergen 1f, 26, 41, 44Initialisierung von Klassen-

beschreibungen 74, 111ffClass, Object 69fFunktion 115, 131fooc 84

Instanz 13, 21isA(), zugehörig zu Klasse 100ff, 109isOf(), abstammend von Klasse 100ff,

109

KKlammern, arithmetischer Ausdruck 27Klasse 13, 21

als Resultattyp 106erzeugen 63

Page 258: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

248___________________________________________________________________________Sachverzeichnis

Klassenarithmetische Ausdrücke 143ffSymbole 146ff

Klassenbeschreibung 14, 62, 75Class 90Dateiname 82, 85Erkennung 216fFunktion 115Initialisierung 74, 111ffObject 89ffooc 80, 83f, 130, 227f

Klassenhierarchie 61ff, 75Dialogsprache 168nachbilden 198ooc 82

Klassenmethode 139f, 152forward() 184Oberklassen-Selektor 141Selektor 140f

Kommandoargumente verarbeiten 125Kommentar, ooc 80, 226Komponente, ooc 80

Sichtbarkeit 40Zugriff 9

Konstante 56fKonstruktor, ctor() 12, 14f, 56, 62f

Argumente teilen 15Class 68fdynamische Bindung 44geto() 163Metaklasse 83new() 153Speicherverwaltung 19fstatisches Objekt 112Unterklasse 42Verkettung 42, 44, 76

LLabel 198Leck, Speicherverwaltung 137lineare Liste 1, 92ffLineOut 186, 189, 239flink, Report 131link-it, Report 131Linksrekursion 25

List 92fflist, testet List 101fListe von Initialisierungen 113load 155, 172, 176longjmp() 24, 171ff, 212f

MMakro 38, 40, 43, 45MallocDebug 137Marken, ooc 134, 229fMath 57fmathematische Funktion 57mehrfache Vererbung 46mem-Funktionen 53, 214Menge 2meta-ctor-loop, Report 131Metaklasse 62f, 75, 80f, 83f

Konstruktor 83neu anlegen 73Oberklasse 85Wurzel 90

Methode 13, 21, 80Aufruf bei dynamischer Bindung 99ersetzen 38, 44, 76finden 121Kopf 82, 86Marke 134struct Method 130Vererbung 38

Minus 27Mul 27munch 113ff, 118, 165, 225, 233Mux 187, 189, 239

NNachricht 13, 21

forward() 181ffName 49ffName 51ff, 207

erzeugen 76new() Speicherverwaltung 3ff, 10ff, 14f,

71, 152fdynamisch binden 139, 142Konstruktor 153

Page 259: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

249___________________________________________________________________________Sachverzeichnis

nm 113, 118, 225Node 150fNullzeiger, delete() 5numerische Fehler 59

OOberklasse 36ff, 43

Metaklasse 85Object 65Zugriff von Unterklasse 41

Oberklassen-Selektor 71f, 76, 105Klassenmethode 141

Object 3f, 12, 15f, 63ff, 69f, 75, 89ff,142f, 182, 235f

Object, Xt-Klasse 197Objekt 13, 21, 75

dynamischer Speicher 18füllen, geto() 162ff, 169importieren 82, 88ffKlassenbeschreibung 62laden, retrieve() 164ff, 169, 233Methoden finden 121persistent 155ff, 168fspeichern, puto() 160ff, 169statisch konstruieren 112Zeiger 108

objekt-orientierter Entwurf 185f, 188,193f, 198f

offsetof() 68, 214ooc 79ff

-D, Symbol definieren 117-dc, Skelett einer Implementierung

85-h, Schnittstellendatei 85-R, Wurzelklassen bearbeiten 90-r, Repräsentierungsdatei 85Anweisung, siehe % und @Aufrufe und Parameter 222, 225fBedingung 231Beispiel 92ffconst 84fDateiname einer Klassen-

beschreibung 82, 85Datenbasis 217fDeklarator 84, 226, 228

ooc ←Eingabeformat 226ffEntwurf 79, 84ff, 91Erkennung der Klassen-

beschreibungen 216fErsetzung 231Hauptprogramm 221Implementierung 87, 215ffImplementierungsdatei 82ff, 86,

228fInitialisierung 84Klassenbeschreibung 80, 83f, 130,

227fKlassenhierarchie 82Komponenten 80Manual 225ffMarken 229fMetaklasse 80f, 83fMethode 80, 82, 86Object 89ffObjekt importieren 82, 88ffReportdatei 90f, 221f, 230ffReportgenerierung 218ffReportname 88Reportsprache, siehe ‘ 87ffRepräsentierungsdatei 81Schleife 231Schnittstellendatei 80fSelektor 83, 87self 80f, 84Unterklasse 80Wurzelklasse, -Metaklasse 90Zeilennumerierung 218, 220Zwischenraum generieren 89

OOCPATH 91, 223, 232opake Struktur 41overloading 16

PParameter und const 210Parametertyp 207fpersistentes Objekt 155ff, 168fPflege 61Point 33f, 73f, 79ffPointClass 73f

Page 260: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

250___________________________________________________________________________Sachverzeichnis

points, testet Point 33, 75polymorphe Funktion 15f, 21, 44postfix, Ausdruck ausgeben 23, 29Prototyp einer Funktion 207Präprozessor-Entwurf 79, 84ff, 91prüfen

Argument 76Funktionstyp 128fObjekt-Import 89fvoid * 99

puto(), Objekt speichern 66, 160ff, 169geto() 163f

QQueue 95queue, testet Queue 95f

RRahmenprogramm 125, 135

Filter 123ffrealloc() 94, 214recursive descent 25

Zuweisung 50Report, ooc 87ff

casts 88, 103checks 89, 103classOf 105, 185const 117forward 183ifmethod 105link 131link-it 131meta-ctor-loop 131result 105return 105selector 105, 183super-selector 106

Reportdatei, ooc 90f, 221f, 230ffReportgenerierung, ooc 218ff

Vorteile und Nachteile 92Reportname, ooc 88Repräsentierungsdatei 44, 81, 85

Unterklasse 73reserviertes Wort 52

respondsTo() 128ff, 134, 229Ressource, Xt 203Ressourcen verwalten 137result, Report 105retrieve() , Objekte laden 164f, 169,

233return, Report 105run, Taschenrechner 187f, 192

Ssave 155Schleife, ooc 231Schnittstellendatei 44, 80f, 85Selbstorganisation 111ffselector, Report 105, 183Selektor 15f, 21, 34, 70, 76, 83, 104

generieren 87Klassenmethode 140fOberklasse 71f, 76, 105Vererbung und Oberklasse 72

self, Empfänger 13ff, 80f, 84Set 2ff, 10setjmp() 24, 171ff, 212fsets, testet Set 4shallow copy 160Shell-Sort 118Sichtbarkeit von Komponenten 40Signale 108fsizeOf() 16, 67size_t 213Sort 133sort, Anwendung von Filter 132ffspeichern, puto() 160ff, 169

Container-Objekte 169Symbole 155ff

Speicherverwaltung 3ff, 10ff, 15, 151,153

C 214Konstruktor 19fLeck 137

späte Bindung siehe dynamische B.Stack 95stack, testet Stack 95fstatic 207

extern 117

Page 261: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

251___________________________________________________________________________Sachverzeichnis

statische und dynamische Bindung 38fstatisches Objekt, Konstruktion 112String 16ffstrings, testet String 17struct Method 130Struktur verlängern 36, 43, 62Sub 26super() 67super-selector, Report 106Symbol 150Symbol-Klassen 146ffSymbol speichern und laden 155ffSymbol vordefinieren, ooc 117Symboltabelle 53ff, 166Symtab 148f, 156ff, 176

TTaschenrechner 185ff, 192, 237teile und herrsche 2translation table 198Typbeschreibung 12fftypedef und const 210Typprüfung

cast() 100ffCodierstandard 102ff

typspezifische Funktion 20

Uüberladen, Funktion 16umwandeln, Zeiger 43f, 209Unterklasse 36ff, 43, 80

Definitionsdatei 73Destruktor 42erzeugen 65Konstruktor 42Repräsentierungsdatei 73Zugriff auf Oberklasse 41

up-cast 37

Vva_-Makros 200, 213Value 27value, Ausdruck bewerten 23, 30

value ←Exception 175fKlassenhierarchie 137ff, 152save und load 155, 165ffVariablen etc. 49ff

Var 55Variable 50, 55variable Argumentliste 14, 200, 213Vererbung 43, 61f, 75

Aggregat 45Bindung 37, 44einfach 36Implementierung 68mehrfach 46Methode 38Oberklassen-Selektor 72

Verkettung, Destruktor und Konstruktor42, 44, 76

void * 3, 37, 44, 109, 208prüfen 99Zeiger auf Funktion 116

Vorrang von Operatoren 31Vorwärtsverweis 207

WWarteschlange 2, 95Wc 123fwc, Anwendung von Filter 123Widget 197fwrapper 198Wurzel-Metaklasse 90Wurzelklasse 90

XX Window System 197Xaw, Athena Widgets 197XawBox 199, 240fXawCommand 199, 240fXawForm 199, 240fXawLabel 199, 240fXButton 199, 201, 240fxbutton, testet XButton 202fxhello, testet XLineOut 200Xlib, X Bibliothek 197

Page 262: Programmierung mit ANSI-Cats/books/ooc_de.pdf · 2015-09-22 · Nicht zuletzt danke ich einmal mehr meiner Familie — und nein, Objekt-Orien-tierung wird mindestens Kaffee und Kuchen

252___________________________________________________________________________Sachverzeichnis

XLineOut 199f, 240fxrun, Taschenrechner mit X 203fXt, X-Toolkit 197Xt 199, 240XtApplicationShell 199, 240f

ZZeiger auf Funktion 11, 211

void * 116

Zeiger auf Klassenbeschreibung 14Zeiger auf Objekt 108Zeiger, generischer 3, 208Zeiger umwandeln 209Zeilennumerierung, ooc 218, 220Zugriff auf Komponenten 9

Makro 40, 43, 45Zuweisung 56

Makro 40, 45Zwischenraum generieren, ooc 89