Objekt C Programmierung (2)

In unserem zweiten Teil zur Object C-Programmierung geht es nun richtig zu Sache. Den gesamten Artikel samt der enthaltenen Listings erhalten Sie auch über unsere Homepage oder die ST-Computer-Mailbox.
Wenn Sie den ersten Teil dieses Kurses verpasst haben, dann kennen Sie vermutlich all die guten Gründe nicht, die Sie bewegen sollten, Object C anstelle von C++, Java oder etwa Smalltalk zu verwenden. Zu Ihrer Motivation seien hier also einige der wichtigsten Punkte aufgeführt, die aus Platzgründen nicht erneut erläutert werden können.

Allgemeine Vorteile

Selbstverständlich können Sie C oder C++ Bibliotheken aus Object-C-Programmen und umgekehrt Object-C-Methoden und Konstruktoren aus C-Funktionen oder C++ aufrufen.br C/C++ Debugger und Tools wie RCS sind ebenso auf Object C Quelltext (beim Debugger nur auf die von Object C erzeugten ".c"-Dateien) anwendbar. Im Gegensatz zu Sprachen wie Delphi, die meist auch nur auf wenigen Plattformen angeboten werden, sind Sie sogar von der Existenz des Herstellers CPR unabhängig, solange Ihre zukünftigen C-Compiler in der Lage sind, die Laufzeitbibliotheken (O-Lib) zu linken und Programme für Zielprozessor und Betriebssystem zu erzeugen. Auf spezielle Vorteile gegenüber C++ oder Java wollen wir in diesem Artikel aber nicht eingehen. Kommen wir daher zur negativen Seite:

Schwächen von Object C

Natürlich hat auch Object C ein paar Schwächen. Derzeit hat der Object C Compiler Probleme (außer auf SUNDS und LINUX), beliebig große Quelldateien (ab etwa 30 KB) zu übersetzen, weil die intern verwendeten Buffer auf 64 K begrenzt sind. Genau gesagt stürzt der Compiler beim Übersetzen einer zu langen Datei ab oder erzeugt für C unverträglichen Quelltext. Wenigstens ist dabei aber sichergestellt, dass sich der Übersetzungsfehler nicht verstecken kann. Das Problem soll aber mit der nächsten Version behoben oder zumindest stark verringert werden.

Wie bei allen DOP-Sprachen kann das Programm bei sorgloser Implementierung eine Methode von einer anderen Klasse erben, als beabsichtigt war. Der Programmierer bestimmt die implizite Vererbung wie in C++ durch die Aufzählungsfolge geerbter Klassen.

Nun gestattet Object C eine "dynamische" Mehrfachvererbung, was bedeutet, ein Objekt (z.B. der Klasse "Motor") kann anstelle des geerbten Objektes einer Klasse (z.B. "Diesel") auch ein Objekt einer anderen Klasse (z.B. "Raketentreibstoff") erben. Weil Objekte beider Klassen (Diesel oder Raketentreibstoff) sowohl unterschiedliche Attribute aufweisen als auch unterschiedliche Methoden erben können, ist hier eine gewisse Programmierdisziplin gefragt. Die dynamische Vererbung kann also viel Programmieraufwand ersparen, ist jedoch nicht ganz ungefährlich.

Die Auswertung der Vererbung zur Laufzeit kostet den Methodenaufruf wie bei vielen anderen OOP Sprachen natürlich je nach Erbtiefe eine entsprechende Geschwindigkeitseinbuße. Ebenso ist der Service des Garbage Collectors nicht umsonst. Bei ungeschickter Programmierung kann ein Programm dann auch sehr langsam werden. Das Referenzhandbuch gibt im Kapitel "Optimierung" aber eine Reihe von Ratschlägen, deren Beachtung auch bei Millionen von Objekten zu sehr schnellen Programmen verhilft.

Die Fragmentationsfreiheit des Speichers beansprucht etwa 30% mehr Speicher, als bei normaler Anforderung nötig wäre. Dafür verbraucht ein Speicherbereich minimal aber auch nur 32 Byte. Unter Windows hingegen wird meist ein Minimum von 4 KB angefordert (auch mit malloc()), was schnell zu einem Swap-Problem ausarten kann. Besonders für Grafikanwendungen ist die maximale Größe von Objekten ein Problem. Diese beträgt nämlich nur 1 KB - ebenfalls ein Tribut an Fragmentationsfreiheit und Geschwindigkeit. Glücklicherweise stellt die OCCBibliothek eine Klasse 'Memory' zur fragmentionsfreien Verwaltung großer Speicher bis zu knapp 64 KB. Jedoch ist OCC noch nicht auf ATARI portiert. Die Funktionen "Memhdl mein = allocmem(bytes);", "Char_ptr ptr = lockmem(mem);" und " unlockmem(mem);" sowie "freemem(mem);" sind aber in jedem Object C Programm verfügbar (definiert in bas.h, durch obj.h geladen) und bieten immerhin normalen Speicher bis zur jeweiligen Plattformgrenze.

Für ATARI ST wird derzeit nur GCC 1.5.4 unterstützt (für 40; DM Bearbeitungsgebühr inkl. Verpackung und Versandkosten bei CPR zu beziehen). Andere C Compiler für ATARI haben keinen geeigneten Präprozessor, und GCC 2.7 läuft bisher nur unter MINIX (und somit z.B. nicht problemslos unter MagicMac). In wenigen Monaten sollte aber auch ein für Object C geeigneter 2.7er GCC-Release vorliegen.

Dateitypen und Übersetzung

Jedes Object C Programm besteht aus mindestens einem Object C Modul. Jedes Object C Modul definiert seine Klassen und weitere normale ".h"-Datei Bestandteile (z.B. Konstanten, Typen und Makros) in eine oder mehrere ".i"-Dateien. Der Object C Compiler übersetzt diese ".i"-Dateien (z. B. "draw.i") dann in ".h"-Dateien (z.B. "draw.h").

In der Kommandozeile werden neben den ".i"-Dateien die zum Modul gehörigen ".m"-Dateien aufgezählt. In ".m"-Dateien können Konstruktoren und Methoden implementiert werden. Zwischen den Konstruktoren und Methoden kann beliebiger C Quelltext (z.B. globale Variablen oder Funktionen) stehen. Eine Methode kann dabei, nur Klassen des eigenen Modul unterstützen (und somit deren Vererbung exportieren), womit bereits ein erheblicher Beitrag zur Modularisierung geleistet ist. Sie sollten deshalb Ihre ersten Projekte innerhalb eines einzigen Moduls implementieren (geben Sie ggf. am Ende der Kommandozeile einen Backslash zur Erweiterung auf die nächste Zeile an). Die Trennung in Module erfordert nämlich eine gewisse Planung.

Werden mehrere Module verwendet, so ist die Modulnummer (Option -mX) auf einen Wert zwischen 0 und 255 zu setzen. Die README-Datei gibt hierzu weitere Informationen. Ein Übersetzungsaufruf für Modul Nummer 1 (0 ist default) könnte also etwa so aussehen: "objc -m001 -dd draw.h draw.i drawl.m draw2.m draw3.m".
Anschließend müssen Sie die entstandenen ".c"-Dateien nur noch wie im README beschrieben mit GCC übersetzen.

Wenn Sie Verwechslungen mit dem Objective C Compiler des GCC befürchten, können Sie "OBJC.TTP" auch in "OC.TTP" umbenennen und "oc" anstelle "objc" in den Übersetzungsaufrufen verwenden.
Beispiel einer Includedatei (siehe Listing 1)

Wie wir sehen, können Klassen auch ohne eigene Attribute bestehen (Fahrer) und eine oder mehrere Klassen erben (Fahrer bzw. Auto). Was bedeutet Vererbung nun eigentlich? Was ist eine Methode? Eine Methode ist zunächst nichts anderes als eine Funktion. Diese Funktion möchte jedoch entsprechend ihrer Klasse einen Speicherbereich mit geeigneten Attributen erhalten. Dieser Speicherbereich wird als Objekt einer bestimmten Klasse bezeichnet. Die Attribute sind die Struktureinträge, die wie in einem Cstruct in den $datBlöcken aufgezählt wurden.

Ein Methodenaufruf für ein bestimmtes Objekt ruft also die Funktion auf und übergibt dieses Objekt (auch Zielobjekt genannt) als erstes Argument. Fügen wir also eine Methode zu unserem Beispiel hinzu:
Beispiel einer Methodendatei: (siehe Listing 2)

Was könnten wir also mit dieser Methode anfangen? Zunächst einmal ist offensichtlich, dass diese Methode die Klasse Person unterstützt (wie ja auch durch $support(Person); ausgedrückt wird). Hätte wir also ein Objekt der Klasse Person, dann wäre der Aufruf "print(o_person);" möglich. Vererbung bedeutet nun, dass wir auch ein Objekt der Klasse Fahrer für einen solchen Aufruf verwenden könnten. Die Methode weiß dann innerhalb des "$class Person"-Blockes nichts mehr über die Herkunft des Objektes und verwendet lediglich das geerbte Objekt Person (also die Attribute von Person).

Erweitern wir unsere Methode nun um eine weitere Klasse Auto: (siehe Listing 3)

Wir können also durch "..Attrname" auf ein lokales Attribut der jeweiligen Klasse - jedoch nicht wie in C++ durch Angabe des Attributnamens auf alle geerbten Attribute - zugreifen. ".." ist hierbei die Kurzschreibweise für "objp>". 'objp', entspricht also dem 'this'Zeiger des C++, reicht aber nicht über die Klassengrenze weg.

Sie werden vielleicht sagen: "Ich möchte aber auch bequem auf die geerbten Attribute zugreifen können." Mit diesem Wunsch haben Sie prinzipiell auch recht. Das Erben von implizitem Attributzugriff trägt jedoch sehr zur Unübersichtlichkeit von Quelltext bei. Das führt soweit, dass in C++ nicht klar ist, ob 'xyz' ein Methodenargument, eine lokale Variable, eine globale Variable oder ein Attribut ist. Falls es ein Attribut ist, müsste noch geklärt werden, zu welcher Klasse es gehört. Falls mehrere Klassen gleichnamige Attribute verwenden, kommt es dann noch zur Namenskollision.

Wie wird also in Object C auf geerbte Attribute zugegriffen? In Object C geht das nur auf zwei Arten:

Durch "@Fahrer" lässt sich also wie mit printf(@Fahrer); der Objekthandle des geerbten Objektes der Klasse Fahrer ermitteln und anwenden. Das gilt schon der Übersicht wegen natürlich auch nur für die erste Erblinie.

Wenn wir z.B. aus Geschwindigkeitsgründen einmal mehrere Erblinien überspringen möchten, dann wird das etwas unschön, bleibt aber immerhin noch lesbar:
(siehe Listing 4)

Wir brauchen also keinen Browser, um alle Dateien nach Zugriffen auf Objekte der Person Klasse zu durchsuchen. Ein einfaches Grep-Programm zur Suche nach ($class Person und $ptr(Person) genügt (nicht unwesentlich, wenn normale TTY-Terminals bzw. Modemverbindungen zur Entwicklung oder Wartung anzuwenden sind).

Tauchen im Quelltext also sehr viele $ptr-Anforderungen auf, dann ist es üblicherweise Zeit, einige Methoden einzuführen oder die Klassenaufteilung zu überdenken. Ist der Quelltext zu langsam, so sollte an kritischen Schleifenaufrufen die eine oder andere $ptr-Anforderung eingebaut oder bestimmte Klassen zusammengefügt werden.

Bevor wir Objekte anwenden können, müssen wir Konstruktoren und Destruktoren zur Erzeugung und Entfernung der Objekte definieren. Das geht folgendermaßen: Beispiel einer Methodendatei mit Konstruktoren
(siehe Listing 5)

Der Konstruktor der Klasse Motor zeigt bereits einen weiteren Object C Vorteil, wir können nämlich im Gegensatz zu C++ den gleichen Namen für Argument und Attribut verwenden. Jedes MotorObjekt speichert nur normale Attributwerte (benötigt also keine Speicheranforderungen für Bilder oder erbt andere Objekte) und kommt somit ohne $cloneBlock und $freeBlock aus. Ein Anweisung der Art "Obj o neuermotor = $clone(o_motor);" würde also den Objektdatenspeicher einfach auf den neu angeforderten Bereich des 'o_neuermotor' kopieren.
Der $freeBlock ist das Gegenstück zum $new-Block. Unterstützt eine Klasse kein Cloning, so sollte ein "$serr("cloning not supported!");" in den $clone-Block eingesetzt werden.

"$serr(Fehlertext);" schreibt den Fehlertext in die Fehlerdatei DbgOut des aktuellen Verzeichnisses und erzwingt die Ausgabe eines Fehlerbaumes (auch sehr hilfreich für Tester von Beta-Versionen).

Der Person-Konstruktor übernimmt nur zwei der Attribute aus der Argumentliste. Das Adressen-Attribut setzt er auf einen leeren String. Es ist nämlich nicht immer sinnvoll, alle Attribute als Argument an den Konstruktor durchzureichen. Besonders dann, wenn im Verlauf der Programmentwicklung weitere Attribute hinzukommen. Oft ist es günstiger, einen geeigneten Default-Wert zu verwenden und eine Set-Methode (z.B. setadresse(o_person, adresse);) anzubieten.

Im Fahrer-Konstruktor wird ein neues Person-Objekt erzeugt und dem hierfür vorgesehenen Speicherplatz

@Person zugeordnet. Der $clone-Block erzeugt eine Kopie hiervon und der $freeBlock gibt diese frei. Alle Bestandteile - Konstruktor, Destruktor und Cloning funktionieren also perfekt. Leider haben wir auch hier nur den leeren Adressen-String in dem Person-Objekt gespeichert und müssten bei Bedarf eine geeignete Adresse per "setadresse(o_fahrer, adresse);" zuweisen.

Ein anderer Ansatz wurde im AutoKonstruktor verwendet. Hier wird gleich ein Objekt der Klasse Motor

übergeben und eine Kopie hiervon verwendet. Das erspart natürlich alle Parameterangaben, erfordert jedoch ein bereits vorliegendes und auch außerhalb des Auto-Destructors zu zerstörendes Referenzobjekt. Es wäre beispielsweise sinnvoll, in einem Array Motor-Objekte verschiedener Motortypen zu speichern und dem Anwender bei der Zusammenstellung seines Autos die Wahl des Motors zu überlassen. Sie haben sicher schon bemerkt, dass @Fahrer auf noobj (O) gesetzt wurde. Für Null-Objekte gibt es ein paar einfache Regeln:

Der Fahrer des Autos wäre jetzt nicht definiert und Aufrufe aller über Fahrer geerbten Methoden des Autos würden nun die Ausgabe des Fehlerbaumes erzwingen.
Wir brauchen also noch eine Methode "set_fahrer(o_auto, o_fahrer);". Sie merken vielleicht jetzt schon, dass die Überlegungen, ob ein Objekt wie hier 'o_fahrer' geklont wird oder einfach dem @Fahrer des Auto-Objektes zugewiesen wird, von großer Bedeutung für den gesamten Quelltext sind und auch entsprechend dokumentiert werden müssen. Am gefahrlosesten ist es immer, lokale Kopien der Daten in Objekten zu halten, bei deren Freigabe dann auch nichts schiefgehen kann. Denn anders als Java verwendet Object C keine Rechenzeit darauf, Zugriffszähler auf Objekte beim jedem Austritt aus Methoden zu verwalten, was natürlich etwas -mehr Sorgfalt vom Programmierer verlangt. Die OCC-Bibliothek (über 80 Dateien) ist aber der lebende Beweis für die relative Problemlosigkeit dieses Konzeptes. (siehe Listing 6)

Zur Übersetzung des Programmes ist nun "objc -dd auto.h auto.i new.m auto.m main.m" anzugeben, wobei dem Compiler die Reihenfolge der Argumente egal ist. Übersetzen Sie anschließend alle C-Dateien mit dem gcc (wie im README beschrieben).

Denkaufgabe

Erweitern Sie das Programm um die Methoden "set_geschwindigkeit(o_motor, hkm);", "tanke_voll(o_auto);", "fahre strecke(o_auto, km);" und geben Sie je nach Geschwindigkeit den aktuellen Tankinhalt aus.

Da wir uns stark dafür interessieren, wie viele Programmierer sich für den Object-C-Kurs in der ST-Computer & ATARI-Inside interessieren, würden wir uns über eine kurze Nachricht an die Redaktion (Z.B. per Postkarte, Telefon oder Fax) sehr freuen.


Andreas Guthke
Aus: ST-Computer 07 / 1997, Seite 37

Links

Copyright-Bestimmungen: siehe Über diese Seite