Objektorientierte Programmierung mit C

Jeder, der schon mal das Vergnügen hatte, größere Programme zu schreiben, wird sicher zugeben, daß sich gewisse Strukturen, seien es Algorithmen oder Datenverbände, besonders dazu eignen, viele der anfallenden Probleme mehr oder weniger elegant zu lösen, und daß sich diese ständig nahezu unverändert in seinen Werken wiederholen. Leider nur nahezu. Die kleinen Unterschiede geben dem Speicherplatz- und Rechenzeitbewußten genug Anlaß, jedesmal eine neue Implementation vorzunehmen. Die Software-Krise ist das Resultat dieser Denkweise.

Diese Tatsache ist uns schon seit langem bekannt und betrifft vor allem Software-Hersteller, die ihre Programme für jeden Kunden neu entwerfen. Modularisierung war der erste Ansatz zur Lösung. Ohne sie würde es wohl keine anwendbare Software mehr geben. Finnen, die sich nur mit der Herstellung solcher Module (im allgemeinen Funktionsbibliotheken) beschäftigen, sind aufgetaucht und entlasten Programmierer von ständig wiederkehrenden Aufgaben. Probleme tauchen aber wieder auf, wenn man Teile dieser Module ändern möchte. Ohne Quelltexte ist dies unmöglich, mit ihnen teuer und langwierig; wer möchte sich schon seitenweise mit fremdem Programm-Code auseinandersetzen? Langer Rede kurzer Sinn: eine universelle Lösung muß her! Eine Lösung, die es uns erlaubt, vorgefertigte Software-Bausteine zu verwenden und zu erweitern, ohne den Quelltext zu besitzen.

OOP

Die objektorientierte Programmierung (OOP) scheint es zu sein. Sie führt zusätzlich zu den uns schon bekannten Sprachkonstrukten das Objekt ein. In konventionellen, also strukturierten Programmiersprachen sind Daten und Algorithmen lose im Quelltext miteinander verbunden. Ein .Objekt hingegen ist eine Sammlung von Daten und den Routinen, die mit diesen Daten umgehen. Da dieser Artikel vor allem den C-Freunden gilt, werde ich in diesem zusammenfassenden Kapitel eine C-ähnliche Syntax verwenden, die so zwar nicht in C++ oder Objective-C vorkommt, die aber die Betrachtung wichtiger Bestandteile der OOP erleichtert. Ein Objekt ist dann also eine C-Struktur, in der zusätzlich zu den Variablen auch noch Funktionen, die mit diesen Variablen umgehen, definiert werden. Diese werden mit derselben simplen Syntax aufgerufen, wie man einzelne Variablen einer Struktur anspricht, zum Beispiel: fenster.zeichnen. Diese Vereinigung der Daten und der dazugehörigen Algorithmen ist die einzige Gemeinsamkeit unter den objektorientierten Programmiersprachen, die inzwischen von Ada bis Smalltalk reichen und mehr oder weniger objektorientiert sind. Ada bietet das Minimum: dadurch, daß nur Funktionen und Prozeduren, die im Rumpf eines Objekts definiert wurden, auf dessen Variablen zugreifen dürfen, hat man den Vorteil der Verkapselung, einer Art galvanischen Trennung, durch die unsaubere Programmierung verhindert wird. Ein gewichtiger Grund für das amerikanische Verteidigungsministerium; uns jedoch interessieren weitere Features zum Beispiel von Smalltalk, der objektorientierten Programmiersprache schlechthin. Und da wäre an erster Stelle die Vererbung zu nennen. Es ist Ihnen sicher schon aufgefallen, daß sich viele Strukturen aus der realen Welt nur in feinsten Details unterscheiden: eine Chromkassette unterscheidet sich von einer Metallkassette nur in der Beschaffenheit des Bandes; das Gehäuse kann völlig identisch sein. Auch die Datenstrukturen ähneln sich: ein Fenster hat mit einer Dialogbox zum Beispiel die Koordinaten gemeinsam (Variablen) und die Notwendigkeit den Hintergrund zu sichern (Funktionen). Man müßte nun ein Objekt definieren, das die gemeinsamen Eigenschaften beider Strukturen besitzt. In unserem Fall wäre dies also ein Rechteck, das, bevor es gezeichnet wird, den Hintergrund sichert. Die Methode, so werden Funktionen in OOP genannt, die dies tut, nennen wir für unser Beispiel rechteck.retten. Nun definieren wir zwei unterschiedliche Nachkommen dieses Objekts, die automatisch alle Eigenschaften erben, und ergänzen diese entsprechend. Sehen Sie dazu die Abbildung 1. Ohne daß in dem Objekt fenster die Methode retten definiert oder deklariert wurde, kann sie aufgerufen werden: fenster.retten. Wenn vererbte Methoden nicht die gewünschte Funktion erfüllen, können sie überladen, ergänzt oder gelöscht werden. Durch die Vererbung wird dem Programm eine hierarchische Struktur aufgesetzt. Wenn wir eine grafische Benutzeroberfläche als Beispiel nehmen, könnte diese Hierarchie wie in Abbildung 2 dargestellt aussehen.

In strukturierten Programmiersprachen wird meistens frühes Binden angewandt, d.h. daß ein Aufruf einer Funktion und die Funktion selbst bereits während der Übersetzung miteinander verbunden werden. In puren objektorientierten Sprachen wird jedoch das späte Binden angewandt. Es ist dadurch möglich, objekt.retten zu schreiben, ohne daß der Compiler weiß, welches Objekt eigentlich gemeint ist (fenster, rechteck, dialogbox...). Erst wenn der Platzhalter objekt durch das tatsächliche Objekt ersetzt wird, also während der Laufzeit, weiß das Programm, wohin es denn nun springen muß.

Das waren die wichtigsten objektorientierten Features. Wir werden uns nun spezialisieren und nicht mehr OOP allgemein betrachten, sondern die Hybridsprachen. Eine objektorientierte Hybridsprache ist ein Amalgam aus einer konventionellen Programmiersprache und objektorientierten Features. So wurde zum Beispiel Pascal zu Object Pascal erweitert. Wir werden aber C++ und Objective-C betrachten, da C von den meisten Programmierern eingesetzt wird und beide Programmiersprachen für den ST verfügbar sind. Ich gehe in dem Artikel also davon aus, daß Sie ANSI-C gut beherrschen. Da ich viele spezielle Begriffe verwenden muß, sollte es an der Zeit sein, diese kurz zu erläutern. Der Unterschied einer Deklaration zu einer Definition, um den es hier oft geht, wird in der Literatur nicht immer sehr gut erklärt. Eine Deklaration sagt dem Compiler lediglich, welchen Typs eine Variable ist, die bereits anderswo definiert wurde oder erst später definiert wird (Vorausdeklaration). Sie können diese Variable nun verwenden, und der Compiler wird wissen, daß er nach der Definition suchen muß. Diese sagt dem Übersetzer, daß er tatsächlich eine Variable eines Typs anlegen soll. Das selbe gilt für den Unterschied zwischen einer Klasse und einem Objekt: die Klasse ist die Deklaration, wie ein Objekt auszusehen hat, und das Objekt ist eine Instanz, eine Definition also. Instanzierung ist der Vorgang, bei dem aus einer Klasse ein Objekt geschaffen wird. Auch Begriffe wie Routine, Funktion und Algorithmus bergen kleine Unterschiede, werden hier aber als äquivalent angesehen. Eine Methode ist eine Funktion innerhalb eines Objekts. Mit Methoden sendet man Nachrichten an das Objekt. Das sollte Sie aber nicht verwirren: eine Nachricht ist im Grunde genommen nichts als ein Funktionsaufruf. Noch ein Wort zu den Listings und Abbildungen. Diese sind soweit wie möglich aufeinander abgestimmt und erfüllen keine besondere Funktion, außer daß sie die Erläuterungen im Text bildlich unterstreichen.

C++

Abb. 1: Die automatische Vererbung aller Eigenschaften
Abb. 2: Objekthierarchie

Wie C wurde auch C++ an den Bell-Laboratorien entwickelt. Ziel des Projekts war, die bereits hinreichend bekannten Schwächen von C zu eliminieren und objektorientierte Zusätze mit der Sprache zu verschmelzen. Der Sprachumfang wuchs dadurch beträchtlich und brachte neue Schwierigkeiten. So blieb es nicht bei der Version 1.2. Heute unterstützen alle wichtigen Compiler auf dem PC C++ 2.0. Die allerneueste Version 2.1 brachte keine wichtigen Neuigkeiten mehr; dadurch daß C++ aber immer wichtiger wird, sah sich ANSI veranlaßt, die Sprache zu standardisieren. Der Entwurf des Standards ist noch immer im Gange.

Was ist neu? Wir werden uns zuerst die Erweiterungen anschauen, die eigentlich nichts mit OOP gemeinsam haben. Zuerst wurde C++ an das ANSI-C an gepaßt beziehungsweise umgekehrt. Und wie es sich für eine neue Sprache auch gehört, wurde eine neue Art der Kommentare eingeführt, schließlich sollte man das Neue bereits an der ersten Zeile erkennen können. Sie werden mit "//" eingeführt und enden mit dem Ende einer Quelltextzeile. Mega in.

Weiterhin wurde call by reference implementiert, um Funktionen, die mehr als einen Parameter zurückgeben, zu vereinfachen. In C hat man das allgemein so erledigt:

funktion(&parameter1, &parameter2);

Der Funktion werden also Zeiger auf die Variablen übergeben. Um an die eigentlichen Werte zu gelangen, müssen die Zeiger erst dereferenziert werden:

int funktion(int *par1 pointer, ...)
    {*par1_pointer += 10;}

In C++ geht das einfacher, indem man den Referenzoperator verwendet:

int funktion(int& parameter1, ...)
    { parameter1 += 10;}

Der Aufruf dieser Funktion sieht dann so aus:

funktion(parameter1, parameter2);

Es werden bei der Übergabe nicht Kopien der Variablen angelegt, sondern Zeiger übergeben, die aber automatisch dereferenziert werden. Weiterhin ist es bei der Parameterübergabe möglich, sogenannte Default-Parameter zu setzen. Nehmen wir an, Sie hätten eine Funktion, die ein Fenster öffnet. Wird beim Aufruf ein Name übergeben, soll das Fenster genau diesen bekommen. Neu sonst.

int open_window(char *name = "Neu") {...}

Nun sind beide Aufrufe möglich und auch legal:

open_window("complex.h"); open window();

Eine Form der manuellen Optimierung in C++ ist die Möglichkeit, Funktionen als inline zu deklarieren. Der Compiler ersetzt dann jeden Aufruf mit der Funktion selbst. Das verbraucht zwar Speicherplatz, macht aber die gesamte Parameterübergabe über den Stack überflüssig. Einfache Funktionen, die oft aus einer Schleife heraus aufgerufen werden, sollten inline sein, wei 1 dadurch die Ausführungszeit deutlich verkürzt wird.

Das nächste Feature hat schon mehr mit OOP gemeinsam. Das Überladen (overloading) wird angewandt, damit Funktionen (oder Operatoren!) gleichen Namens, die mit unterschiedlichen Parametern arbeiten, mehrmals definiert werden können:

int laden(BILD *raster); 
int laden(TEXT *dokument);

In der Version 1.2 war es noch notwendig, globale Funktionen, die überladen werden sollten, zu deklarieren; das ist jetzt nicht mehr so. Sehr interessant ist das (Iberladen von Operatoren, denn dadurch können zum Beispiel komplexe Zahlen mit dem "+" addiert werden.

complex& operator + (complex& x, complex& y) {
    return(complex(x.real + y.real, x.imag +y.imag));
}

Die Funktion complex ist ein Konstruktor und wird weiter unten erklärt. Wenn Sie nach dem obigen Beispiel alle Operatoren definieren, können Sie mit komplexen Zahlen rechnen, als wären diese vom Typ float:

complex a, b, c;... a = b + c;

Da in C "+" für Strukturen nicht definiert ist, müßten Sie eine Funktion aufrufen, die die beiden Zahlen addiert:

a = add(b, c);

Eine deutliche Steigerung der Lesbarkeit also. Überladbar sind nahezu alle Operatoren, nicht jedoch deren Priorität und Assoziativität.

Unter anderem sind auch new und delete seit der Version 2.0 überladbar. Sie ermöglichen das dynamische Allozieren von Variablen; auch Felder und Objekte können alloziert werden. Sie stellen also eine Art Ersatz für Funktionen wie malloc und free dar, mit dem entscheidenden Vorteil, daß nicht der Speicher für zum Beispiel eine Variable angefordert wird, sondern die Variable selbst:

varpointer = malloc(sizeof(int)); 
new int var; 
new charname[128]; 
delete name;

Aber widmen wir uns endlich den objektorientierten Neuerungen. Betrachten Sie die folgende Struktur:

struct RECHTECK {// Variablen 
int x, y, w, h; 
// Funktionen 
void retten(void); friend void bewegen(void); int löschen(void) {...}}

Wie Sie sehen, wurde struct um die Fähigkeit erweitert, Funktionen innerhalb des Blockrumpfes zu deklarieren, sogar zu definieren. Der Vorteil ist, daß die Funktionen freien Zugriff auf die Variablen haben, die sich in derselben Struktur befinden, und zwar ohne lästige Operatoren wie "." oder "->" sondern in derselben Art, wie dies bei lokalen Variablen geschieht. Wenn nun eine Instanz dieses Typs definiert wird, wird Platz für die Variablen und die Zeiger auf die Funktionen geschaffen. Die Funktionen selbst befinden sich natürlich nur einmal im Speicher und werden nicht mitkopiert. Eine Routine kann, wie oben schon erwähnt, wie eine Variable aufgerufen werden, zum Beispiel:

struct RECHTECK rechteck1; 
    rechteck1.retten();

Die Deklaration einer Struktur ist oft der falsche Platz für Funktionsdefinitionen. Deswegen wurde ein Mechanismus entwickelt, der wie folgt aussieht:

void RECHTECK::retten(void) {...}

Vor den Funktionsnamen kommt der Klassenname, getrennt durch "::". Auch hier kann man die Funktion wie oben geschildert aufrufen. Möchte man aber auf diese auch außerhalb zugreifen können, muß man sie als friend deklarieren und definieren. Damit sagt man dem Compiler, daß dies eine lose, nicht zur Struktur gehörende Funktion ist, die aber freien Zugriff auf die Variablen hat, die dort definiert wurden. Man hat nun freie Wahl, was den Aufruf dieser Funktion angeht:

rechteck1.bewegen(); bewegen();

Alle diese Mechanismen erweitern eine Struktur, bieten aber noch immer nicht die gewünschten Eigenschaften. Das Schlüsselwort class erweitert die Strukturen um Vererbung und alles sonstige, was noch fehlt! Betrachten Sie dazu Listing 1. Wie Sie sehen können, ähneln sich struct und class. Die Unterschiede machen das Salz in der Suppe aus. Da ist zunächst ein Doppelpunkt nach dem Klassennamen, der die optionalen Vaterklassen einleitet. Eine Vater- oder Superklasse gibt der Programmierer an, um dem Compiler anzudeuten, daß deren Eigenschaften an die hier beschriebene Klasse zu vererben sind. Man ist nicht auf einen Namen beschränkt, sondern kann Eigenschaften mehrerer Vaterklassen erben (multiple inheritance). Dabei kann man zusätzlich angeben, ob die Superklasse privat oder public vererbt wird. Damit hat es folgendes auf sich: die Verkapselung verbietet dem Programmierer in objektorientierten Programmiersprachen den Zugriff auf geschützte Bereiche. Geschützt sind zum Beispiel in Objective-C automatisch alle Variablen; die Methoden sind frei zugänglich. In C++ ist man jedoch weitergegangen und hat mit drei Schlüsselwörtern public, private und protected die Möglichkeit, die Zugriffsrechte frei zu bestimmen. Diese Schlüsselwörter bewirken, daß alle nachfolgenden Deklarationen und Definitionen in eine dieser Gruppen eingestuft werden. public steht für Bereiche, auf die sowohl aus einer Klasse heraus als auch von außerhalb zugegriffen werden kann, private und protected sind geschützte Bereiche, mit denen nur zugehörige Funktionen umgehen können. Vererbt man eine Klasse mit public, bleiben die Bereiche public und protected unverändert und werden so auch an den Nachfolger weitergegeben. private-Bereiche sind für den Nachkommen nicht sichtbar. Vererbt man die Klasse jedoch private, werden public und protected privat. Ein etwas komplexerer Zusammenhang, den man vereinfacht so darstellen könnte: public gehört allen, protected gehört mir und meinen Nachkommen, und private gehört nur mir.

Eine höchst interessante und sinnvolle Möglichkeit, die sich bei den Klassen anbietet, sind Konstruktoren und Destruktoren. Der Name impliziert es schon: ein Konstruktor erschafft. Er konstruiert eine Instanz einer Klasse. Sie können dazu eine oder mehrere (durch Überladung) Funktionen bereitstellen, die beim Entstehen einer Variable dieser Klasse automatisch aufgerufen werden. Diese Funktionen haben denselben Namen wie die Klasse selbst. Im Listing 1 sind drei mögliche Konstruktoren definiert, die folgendermaßen benutzt werden können:

class Rechteck rechteck1; class Rechteck rechteck2(0, 0, desk_w, desk_h); 
class Rechteck rechteck3(rechteck2);

In der ersten Zeile wird eine Variable in üblicher Form definiert, zusätzlich werden aber auch die Klassenvariablen (in diesem Fall die Koordinaten des Rechtecks) auf 0 gesetzt. Die zweite Definition initialisiert mit ganz bestimmten Werten, und die dritte kopiert rechteck2 nach rechteck3. Selbstverständlich können Konstruktoren auch bei dynamischer Definition angewandt werden:

new class Rechteck rechteck4(rechteck2);

In derselben Weise, in der Variablen zum Beispiel nach dem Beenden einer Funktion automatisch vom Stack gelöscht werden, geschieht dies auch bei den Klassen, wobei hier aber vor dem Löschen der De-struktor aufgerufen wird. Der Destruktor ist, ähnlich dem Konstruktor, eine Funktion mit dem Namen der Klasse, vor dem aber noch der Negierungsoperator, die Tilde (~) steht. Haben Sie in Ihren Konstruktoren zum Beispiel dynamisch Speicher allozfert, kann dieser hier automatisch gelöscht werden. Ein Destruktor hat selbstverständlich keine Argumente. Sehen Sie dazu Listing 2. Dort sehen Sie auch eine Variable this, die nirgends deklariert oder definiert wird. this ist ein Schlüsselwort und ein Zeiger auf das Objekt, dessen Funktion gerade abgearbeitet wird. Weiter unten, wenn Objective-C zur Diskussion stehen wird, werden wir uns eingehender damit beschäftigen. Dort heißt diese Variable self.

C++, so hat man sich entschieden, verwendet normalerweise das frühe Binden, das bedeutet, daß alle Aufrufe einer Funktion bereits beim Übersetzen bekannt sein müssen. Dies muß aber nicht immer der Fall sein: Nehmen wir zum Beispiel eine Klasse, der man beliebige Objekte unterschiedlicher Klassen übergeben kann. Die Objekte werden in der Klasse verwaltet und warten zum Beispiel auf einen Aufruf: eachElementPerform:redraw. Dieser Aufruf veranlaßt unsere Klasse, bei jedem Objekt redraw aufzurufen. Die Anwendungsmöglichkeiten einer solchen Klasse sind immens vielfältig und sollten auch in C++ verfügbar sein. Sie sind es auch! Virtuelle Funktionen ermöglichen das späte Binden und sind im Listing 2 daran zu erkennen, daß ihnen virtual vorangestellt ist. In der Praxis geht man folgendermaßen vor: Man definiert eine Klasse, die so gar nicht angewandt wird. Diese Klasse nennen wir abstrakt. Wir deklarieren bereits in dieser abstrakten Klasse Funktionen, die in ihren Nachfolgern definiert werden müssen. Man vererbt dadurch also einem ganzen Stamm die entsprechende Funktion, ohne sie eigentlich definiert zu haben. Sollte in einem Nachfolger die Funktion nicht implementiert sein, wird bei dessen Vater nachgeschaut. Sollte sie auch dort nicht definiert sein, wird das ganze wiederholt, bis man zur abstrakten Klasse selbst gekommen ist. Da sie dort auch nicht definiert ist, entsteht ein Laufzeitfehler. Um solchen Fehlern Vorbeugen zu können, gibt es in C++ die Möglichkeit der virtuellen Funktionen:

virtual void beispiel(void) = 0;

Der Compiler wird sich weigern, eine Klasse, die solche rein virtuellen Funktionen enthält, zu instanzieren. Auch mit abgeleiteten Klassen wird es unmöglich sein, eine Variable zu definieren, solange sie keine Definitionen dieser Funktionen enthalten.

So, das waren die wichtigsten Eigenschaften von C++. Ich habe einige Details der Sprache einfach weggelassen, da sie den Überblick erschweren würden. Wie bereits gesagt, ist C++ noch nicht standardisiert, und es gibt noch keinen kommerziellen Compiler für den ST. Sollte sich an dieser Lage etwas ändern, werden wir das sofort berichten; auch würde dann einem Tutorial nichts mehr im Wege stehen. Die Eingefleischten und Speicherreichen können g++ von GNlJ benutzen, um sich erste Eindrücke von C++ zu verschaffen.

Objective-C

Ganz im Gegensatz zu C++ versucht Objective-C nicht, die bewährte Sprache in ihrer Syntax zu erweitern und zu verbessern, sondern gibt dem Programmierer einige Sprachkonstrukte, mit denen es möglich ist, objektorientiert zu programmieren. C ist hier nur das Mittel zum Zweck, denn auch Pascal, Fortran oder eine beliebige andere Programmiersprache hätte dazu benutzt werden können, diese Erweiterungen, die übrigens bewußt sehr an Smalltalk erinnern, zu tragen, wäre sie so flexibel wie C. Dadurch, daß sich diese Neuerungen auch optisch von dem restlichen Code unterscheiden, ist man in der Lage, Objektorientiertes sofort vom Konventionellen zu unterscheiden. Das ist wichtig, denn Objective-C führt die Erkenntnisse von Smalltalk knallhart ein: so wird von der Funktion Abstand genommen; man verschickt nunmehr Nachrichten an Objekte, auch wird nur das späte Binden angewandt. Anders als in C++ kann man hier die Objektorientierung nicht verringern, man hat aber noch immer die Chance, den Schwerpunkt zu verschieben. In seinem Buch, über das Sie in der Rubrik Buchbesprechungen mehr lesen können, beschreibt Brad Cox, wie Objective-C implementiert werden kann. Es ist aber bereits ein kommerzieller Precompiler für diese Sprache verfügbar, so daß dem Motto: „Programmieren geht über Studieren“ nicht mehr viel im Wege steht. Doch auch Objective-C ist noch in nicht in seiner Entwicklung abgeschlossen, so daß wir uns hier auf die Features beschränken, die in dem Buch erklärt und von dem uns zur Verfügung stehenden Compiler unterstützt wurden.

Objective-C erledigt die Verkapselung eleganterweise, indem es pro Datei eben nur eine einzige Objektdefinition erlaubt. Abbildung 3 zeigt eine solche Definition. Nach dem Anfangszeichen "=", das vor allem die Übersetzung erleichtert, folgt der Klassenname und der Name der Klasse, deren Eigenschaften geerbt werden sollen. Im Unterschied zu C++ ist die Vererbung nicht optional, sondern bindend. Alle Wege führen zu Object, dem Vater aller Klassen, der bereits wichtige Methoden enthält. Dem Vorgänger folgen Variablendefinitionen, wie bei einer C-Struktur, und denen wiederum die Liste aller Methoden, die mit "=:" abgeschlossen wird. In Objective-C gibt es zwei Arten von Methoden: die einen, zu erkennen am "+" zu Anfang der Definition, erzeugen Instanzen einer Klasse nach dem Prinzip der Konstruktoren in C++ und heißen Factory-Methoden; die anderen, die mit einem beginnen, heißen Instance-Methoden und sind Methoden, die auf die Instanzen angewandt werden können. Der Unterschied erklärt sich am einfachsten mit einem Programmstück:

id myObject; myObject = [Object new];
                        [myObject free];

Dabei ist new eine Factory- und free eine Instance-Methode. Der Typ id, mit dem Objekte beliebiger Klasse belegt werden können, wird in derselben Art angewandt, wie das mit allen anderen C-Typen üblich ist. Das Versenden einer Nachricht geschieht wie in Smalltalk in zwei eckigen Klammern und verhält sich wie ein C-Ausdruck, d.h. daß er einen Wert zurückgibt, beliebig tief geschachtelt und aus anderen Ausdrücken aufgerufen werden kann. Der Typ des zurückgegebenen Wertes ist, wie die Parameter einer Nachricht, normalerweise id, kann aber mit einem C-Castoperator explizit umgewandelt werden. Aus einer Methode heraus kann man auf alle Variablen, die in der Klasse definiert oder in die Klasse vererbt wurden, zugreifen, als seien sie lokal zu der Methode. Von außen jedoch hat man nur Zugriff auf die Methoden, die Variablen bleiben versteckt. Methoden, die vererbt wurden, können problemlos überladen oder ergänzt werden.

In dem obigen Beispiel, in dem myObject initialisiert wurde, gab die Methode new einen Wert des Typs id zurück. Damit new die Instanz, die sie gerade erzeugt hat, zurückgeben kann, braucht sie die Information über sich selbst. self ist das Objekt sebst. Das geht sogar so weit, daß Methoden, die in einem Objekt definiert sind, auf andere zugreifen können, die zum selben Objekt gehören:

 - zeige {[self draw];}

Sollte eine Methode nicht in der Klasse selbst definiert sein, wird, wie in C++, beim Vater nachgeschaut. Sollte die Methode nicht zu finden sein, wird eine Fehlerroutine abgearbeitet.

Es passiert häufig, daß vererbte Methoden nicht mehr ganz ihrem Zweck nachkommen. Es wäre sinnlos, diese durch vollständiges Überladen ganz wegzuwerfen. Man möchte also eine Methode der Vaterklasse verwenden, obwohl man sie überladen hat. Die Factory-Methode new zum Beispiel wird oft zu Demonstrationszwecken herangezogen:

+ new {return([super new]);}

super bezieht sich auf den Vater und vermeidet Rekursion, die auftreten würde, wenn man self verwenden würde. Die neue Methode kann noch zusätzlich ergänzt werden. In unserem Beispiel könnte man die Variablen initialisieren.

Die Methoden, die hier aufgeführt wurden, verwenden keine Argumente. Aber auch diese können verwendet werden. Die Methode point aus der Abbildung 3 benutzt Argumente. Damit Sie ein Gefühl für das Aussehen der Sprache bekommen, habe ich ein kurzes Listing zusammengestellt (Listing 3).

Wie bereits weiter oben erwähnt, ist die Basisklasse von Objective-C das Object. Object ist eine Klasse aus dem Standardpaket. Die Idee hinter OOP ist ja, daß vorhandene und ausgetestete Klassen an den Kunden geliefert werden, der diese erweitern und ergänzen kann, ähnlich wie Elektroniker das mit ihren ICs machen. Dort wird Objektorientierung scheinbar schon lange sehr erfolgreich angewandt. Das soll nun auch mit der Software möglich sein (Software-IC); nur wenn keine Klassen existieren, kann man diese auch nicht erweitern. Deshalb liefert die Firma Stepstone, die die Rechte an Objective-C besitzt und auch einen Compiler produziert, ein Paket sinnvoller Klassen mit ihrem Produkt. Diese enthalten unter anderem komfortable Felder, Collections ähnlich der oben beschriebenen, und Mengen.

Zusammenfassend könnte man sagen, daß Objective-C die Objektorientierung zielstrebiger verfolgt als C++. Letztere Sprache hat aber entscheidende Vorteile; beispielsweise ist der Name nicht rechtlich geschützt. Dadurch existieren auf den Kompatiblen, Unix und dem Mac bereits viele unterschiedliche Compiler. Viele Programmierer betrachten C++ als würdigen Nachfolger von C und steigen deshalb um. Da aber C++ eine umfangreiche Sprache ist und in vielen Punkten von der C-Ideologie ab weicht, stellt sich für mich die Frage, ob es denn nicht sinnvoller wäre, gleich auf eine richtige objektorientierte Sprache wie Smalltalk oder Eiffel umzusteigen, wenn man denn nun überhaupt objektorientiert programmieren will.

= Klassenname: Vaterklasse 
{
        // Variablen im C-Stil
}

+Factorymethode
{
        // Definition
}

-Instancemethode 
{
        // Aufruf einer Factorymethode 
        [Klassenname Factorymethode];

        // Aufruf einer Instancemethode 
        [Objektname Instancemethode];

        // Aufruf einer Methode mit Variablen 
        [Objektname pointX: 0 Y: 0 Color: 2];
}

-pointX: (int) sx Y: (int) sy Color: (int) sc
{
        // Definition
}

=:

Abb.3: Die Methode Point

Literatur:

OOP allgemein:
A. Winblad, S Edward,

Object-Oriented Programming,
Addison Wesley 1990, ISBN 0-201-50736-6

C++:
B. Stroustrup, Die C++ Programmiersprache,
Addison Wesley 1987, ISBN 3-92511872-1

S. Lippman, C+ + Einführung und Leitfaden,
Addison Wesley 1990, ISBN 3-89319-276-6

Objective-C:
Brad J. Cox, Object-Oriented Programming,
Addison Wesley 1986, ISBN 0-201-10393-1

class Rechteck 
{
protected:
    int     x;
    int     y;
    int     w;
    int     h;
public:
    rechteck(void);
    rechteck(int rx, int ry, int rw, int rh); 
    rechteck(Rechteck& r);
    ~rechteck(void);

    void    retten(void);
    friend  void bewegen(void); 
    int löschen(void)
    {
        // Funktionsdefinition...
    }
};

Rechteck::rechteck(void)
{
    x = 0; 
    y = 0;
    w = 0;
    h = 0;
}

Rechteck::rechteck(int rx, int ry, int rw, int rh)
{
    x = rx; 
    y = ry; 
    w = rw; 
    h = rh;
}

Rechteck::rechteck(Rechteck& r)
{
    x = r.x; 
    y = r.y; 
    w = r.w; 
    h = r.h;
}

Rechteck::~rechteck(void)
{
}

void
Rechteck::retten(void)
{
    // Funktionsdefinition...
}

friend void 
bewegen(void)
{
    // Funktionsdefinition...
}

class Fenster: public Rechteck
{
protected:
    int     art;
    char    *fenster_name;
public:
    fenster(char *name) 
    ~fenster(void); 
    virtual redraw(void) = 0; 
    virtual Fenster& öffne(void);
}

Fenster::fenster(char *name)
{
    fenster_name = malloc(strlen(name) + 1); 
    strcpy(fenster_name, name);
}

Fenster::~fenster(void)
{
    free(fenster_name);
}

Fenster& Fenster::öffne(void)
{
    // Funktionsdefinition 
    return(*this);
}
extern id Object;

= Kunde : Object 
{
    char    firma[40];
    char    vorname[40];
    char    nachname[40];
    char    straPe[40];
    int     plz;
    char    ort[40];
}

+ anlegen 
{
    char    buffer[40];

    self = [super new];
    printf("Neuer Kunde:\n\n");
    printf("Firma: ");
    gets(firma);
    printf("Vorname: ");
    gets(vorname);
    printf("Name: ");
    gets(name);
    printf("StraPe: ");
    gets(straPe);
    printf("Postleitzahl: ");
    gets(buffer);
    plz = atoi(buffer);
    printf("Ort: ");
    gets(ort);
    return(self);
}

- (void)löschen 
{
    [self free];
}

- (void)ausgeben 
{
    printf("%s\n%s\n%s\n%s\n%d\n%s\n", 
        firma, 
        vorname, 
        nachname, 
        straPe, 
        plz, 
        ort);
}

=:

int main(void); 

int
main(void)
{
    id  neu;

    id = [Kunde anlegen];
    [neu ausgeben];
    [neu löschen]; 
    return(0);
}

Grischa Ekart
Aus: ST-Computer 07 / 1991, Seite 104

Links

Copyright-Bestimmungen: siehe Über diese Seite