<< Inhalt >> Workshops Inhalt

Workshops / Hintergrundberichte / Buchvorstellungen

1. Workshop: Programmierung in C++ - Teil 3 (von Thomas Richter)

Im heutigen Kapitel des "C++"-Kurses wollen wir nun endlich nicht nur von Punkt- und Linienobjekten reden, sondern solche auch auf dem Bildschirm erscheinen lassen. Zum Glück hilft uns dabei das Betriebssystem weiter, indem es fertige Funktionen zur Verfügung stellt, die eben das leisten. Funktionen - und nicht Objekte! Das AmigaOS ist zu einem großen Teil in "C" und nicht in "C++" geschrieben, und diese älteren Betriebssystemteile kennen das Objektkonzept nicht. Aus diesem Grunde müssen wir uns auch noch mit zwei anderen, bislang nicht behandelten, Konzepten von "C" und "C++" auseinander setzen. Bis jetzt konnte ich Beiden aus dem Wege gehen, da sie "traditionell" schlecht verstanden werden: Referenzen und Pointer. Dennoch sind es wichtige Konzepte, die wir beim Umgang mit dem Betriebssystem brauchen werden.

Referenzen: Die Hardlinks von C++

Eine Referenz ist soviel wie ein "Link" im Dateisystem: Man bezeichnet das selbe Objekt mit zwei verschiedenen Namen. Um dies an einem Beispiel zu illustrieren, erzeuge man zunächst wie im ersten Teil beschrieben ein neues Projekt des Namens "Reference" und füge ein Quellfile des Namens "Reference.cpp" in das Projekt ein. Ferner werden wir auch wieder die "storm.lib" benötigen, die deshalb wie schon in den letzten Folgen in das Projekt mit aufgenommen werden muss.

Folgenden Quelltext bitte als "Reference.cpp" eintippen und kompilieren:

#include 

int main(int argc,char **argv)
{
int a;
int &b = a;

        a = 5;
        cout << "a ist "<<a<<" und b ist "<<b<<".\n";
        b = 4;
        cout << "a ist "<<a<<" und b ist "<<b<<".\n";

        return 0;
}

Die Bedeutung der Zeile "int a" kennen wir bereits: Hier wird eine Variable namens "a" angelegt, die Ganzzahlen aufnehmen kann. Die Zeile darunter beschreibt ein neues Objekt: Das "&"-Zeichen vor dem "b" deklariert eine Referenz auf ein Ganzzahlobjekt, und diese Referenz wird auf "a" initialisiert. Damit ist "b" keine eigenständige Variable, sondern lediglich eine andere Bezeichnung für das "a"-Objekt.

Dies belegt die Wirkung der Zeilen darunter: Kompiliert man das Programm und führt es aus, so erhält man folgende Ausgabe:

a ist 5 und b ist 5.
a ist 4 und b ist 4

Obwohl man zunächst "b" gar nicht berührt hat, ist der Wert von "b" bei der ersten Ausgabe gleich dem Wert von "a". "b" ist lediglich eine Referenz auf "a" und ändert sich, sobald man "a" ändert.

Das Spiel funktioniert auch umgekehrt: Ändert man "b", so ändert man "a" gleich mit. Das belegt die zweite Ausgabe.

Aber wozu nun das Ganze? Der ganze Aufwand, nur um Variablen mit neuen Namen zu versorgen? Dazu ändern wir das Programm etwas ab:

#include 

void Inkrement(int &b)
{
        cout << "b ist "<<b<<".\n";
        b++;
        cout << "b ist nun "<<b<<".\n";
}

int main(int argc,char **argv)
{
int a;

        a = 5;
        cout << "a ist "<<a<<".\n";
        Inkrement(a);
        cout << "a ist nun "<<a<<".\n";

        return 0;
}

In den Zeilen 3 bis 8 haben wir eine Funktion definiert, die als Argument eine Referenz auf eine Ganzzahl übergeben bekommt. Diese Referenz wird zunächst ausgedruckt und dann um eins erhöht: Der Ausdruck "b++" ist dabei die "C" und "C++" Kurzform für "b = b + 1". Danach wird diese Referenz noch einmal ausgegeben - sie sollte dann um eins größer sein.

Im Hauptprogramm darunter erzeugen wir eine Ganzzahlvariable "a", setzen sie auf fünf und geben sie aus. Danach wird "Inkrement" mit dem Argument "a" aufgerufen und danach nochmals ausgegeben.

Kompiliert und startet man nun dieses Programm, so erscheint auf der Konsole folgende Ausgabe:

a ist 5.
b ist 5.
b ist nun 6.
a ist nun 6.

Wie erwartet ist der Wert von "a" zunächst fünf. Beim Aufruf von "Inkrement" erzeugt man eine Referenz auf "a", die "b" heißt - anderer Name, aber gleiche Variable. Nun gut, wenn wir also "b" hier ausdrucken, so ist das nur ein anderer Name für "a", und somit gleicht der Wert von "b" auch dem von "a". Das Erhöhen von "b" in der nächsten Zeile erhöht also nicht nur "b", was man sowieso erwartet hätte, sondern auch "a".

Nun zu einer kleinen Änderung im Programm: Die Zeile

  void Inkrement(int &b)

ändern wir mal in

  void Inkrement(int b)

ohne das "&" Zeichen. Kompilieren und starten wir jetzt das Programm, so gibt das anders als zuvor die Ausgabe:

a ist 5.
b ist 5.
b ist nun 6.
a ist nun 5.

Während wir immer noch "b" erhöhen, bleibt diesmal "a" unverändert. Dies liegt daran, dass wir statt "a" selbst nur eine Kopie von "a" an die Inkrement-Funktion übergeben haben, und nur diese Kopie verändern. Im ersten Beispiel gibt es eine Variable mit zwei Namen, jetzt zwei Variablen, wovon der Inhalt der ersteren - "a" nämlich - in "b" hinüber kopiert wird.

Wer bereits die Programmiersprache "Pascal" kennt, dem wird der eben demonstrierte Unterschied unter den Bezeichnungen "call by reference" und "call by value" bekannt sein: Im ersten Fall "referenzieren" wir "a", wobei "b" stark an "a" gebunden wird, im zweiten Falle übergeben wir nur den Inhalt von "a" und kopieren ihn in "b".

Wozu nun dieser Aufwand? Hätte man nicht "a" direkt in Inkrement() ver- ändern können? Oder die Änderung von "a" direkt im Hauptprogramm vor- nehmen können?

Nun gut, gegen letzteren Einwand kann man eigentlich keine vernünftigen Argumente anbringen - dererlei einfache Aufgaben wie das Hochzählen einer Variablen könnte man durchaus im Hauptprogramm direkt vornehmen... Nun ja, es ging auch mehr darum, Referenzen zu erklären, als ein effektives Programm zu schreiben.

Das Erhöhen im "Inkrement()" kann man ja mal versuchsweise ausprobieren:

void Inkrement(int b)
{
        cout << "b ist "<<b<<".\n";
        a++;
        cout << "b ist nun "<<b<<".\n";
}

Der Compiler merkt dann dazu an:

Error: Identifier "a" not defined,
...rence.cpp, Line 6: a++

Aha! Die Variable "a" gibt's in der "Inkrement"-Funktion gar nicht, sie ist, wie man sagt, nicht "im Scope" dieser Funktion. Variable - außer globalen Variablen, die wir noch nicht betrachtet haben - sind nur innerhalb des geschweiften Klammerpaares erreichbar, in dem sie deklariert wurden. Das "geschweifte Klammerpaar", innerhalb dessen "a" gültig ist, ist hier nun das Klammerpaar des Funktionsrumpfes von "main". "a" ist also eine lokale Variable von "main", und ist darum nicht in "Inkrement" verfügbar.

Wir haben also gesehen, dass das Übergeben eines Objektes an eine Funktion per Referenz erlaubt, dieses Objekt innerhalb der Funktion zu verändern und somit mit diesem Objekt zu arbeiten. Es gibt aber noch einen zweiten Grund dafür, Objekte per Referenz zu übergeben: Oben sagte ich, bei einem Funktionsaufruf ohne Referenz wird das zu übergebene Objekt kopiert: Es wird somit eine Kopie des Objektes erzeugt, und dazu wird ein besonderer Konstruktor - siehe die letzte Folge - aufgerufen: Dieser sah für das Punktobjekt wie folgt aus:

                Punkt(const Punkt &original)
                {
                        ...
                }

Das Originalobjekt wird hier "per Referenz" - dazu das "&" vor "original" - übergeben, und man erzeugt die Kopie zum Beispiel, indem man die einzelnen Komponenten von Hand zuweist.

Das Anfertigen einer Kopie erfordert also den obigen Construktor-Aufruf, den man darum auch den "Copy-Constructor" des Objektes nennt. Vereinbart man keinen Copy-Constructor, so verwendet der Compiler eine fest eingebaute, sehr einfache Routine, die alle Komponenten zuweist. Das ist meist, aber nicht immer, das, was man will. So könnte man innerhalb eines Copy-Constructors etwa die Anzahl der Kopien mitzählen, die man von einem Objekt angefertigt hat.

Auch das Punktobjekt hat einen relativ einfachen Copy-Constructor, aber wenn die Objekte sehr lang werden, wird das Anfertigen einer Kopie auch recht umständlich. All' das erspart man sich, indem man die Parameterübergabe bei komplizierten Objekten per Referenz erfolgen lässt.

In dem Copy-Constructor der letzten Folge steht noch ein "const" vor dem Objektnamen: Dies bedeutet, wie man sich auch leicht denken kann, dass das Objekt innerhalb der Funktion nicht verändert werden darf. Ändern wir hierzu noch einmal unser Referenz-Testprogramm etwas ab:

void Inkrement(const int &b)
{
        cout << "b ist "<<b<<".\n";
        b++;
        cout << "b ist nun "<<b<<".\n";
}

int main(int argc,char **argv)
{
int a;

        a = 5;
        cout << "a ist "<<a<<".\n";
        Inkrement(a);
        cout << "a ist nun "<<a<<".\n";

        return 0;
}

und versuchen dies nun zu kompilieren: Der Compiler beschwert sich nun mit einer Fehlermeldung:

Error: Attempt to assign to const object.
...rence.cpp, Line 6:  b++;

"b" war eine Konstante, also nicht änderbar. Auch bei einer Übergabe "als Wert" ist das Original nicht änderbar, man ändert höchstens eine Kopie, aber diese Kopie will auch angelegt sein. Und das ist eben bei komplexen Objekten gegebenenfalls recht umständlich.

Pointer: Die Wegweiser von C++

Referenzen sind ein recht neues Konzept und sind in der Muttersprache "C" nicht verfügbar. Stattdessen gibt es in "C" ein älteres Konzept, das es genauso in "C++" gibt: Dies sind sogenannte "Pointer". Alles, was man mit Referenzen machen kann, kann man auch mit Pointern machen. Aber Pointer können noch viel mehr! Da AmigaOS aber nun in "C" und nicht in "C++" geschrieben wurde, gibt's keine Referenzen, sondern nur Pointer - also müssen wir uns damit (auch noch) beschäftigen.

Traditionell wird leider das Pointerkonzept schlecht dargestellt, und insbesondere beim Einstieg in eine Programmiersprache hat man es oft schwer damit. Ich werde zumindest versuchen, diese Tradition zu brechen; dazu wandeln wir das Programm nochmals ab:

#include 

void Inkrement(int *b)
{
        cout << "*b ist "<<*b<<".\n";
        (*b)++;
        cout << "*b ist nun "<<*b<<".\n";
}

int main(int argc,char **argv)
{
int a;

        a = 5;
        cout << "a ist "<<a<<".\n";
        Inkrement(&a);
        cout << "a ist nun "<<a<<".\n";

        return 0;
}

Auch das tut wieder genau dasselbe wie das Programm oben, nur mit Pointern statt mit Referenzen.

Das Funktionsargument von "Inkrement" ist diesmal aber keine ganze Zahl, auch keine Referenz auf eine solche, sondern ein Pointer auf eine ganze Zahl - dies ist die Bedeutung des Sternchens vor "b".

Ein Pointer ist so etwas wie eine Hausnummer einer Variablen: Wenn man die Hausnummer hat, kennt man zwar nicht die Variable selbst, weiß aber, wo sie wohnt und wie man sie erreichen kann. Das ist fast genauso gut. Ist etwa, wie hier, "b" diese Hausnummer, so ist "*b" der Wert der Variablen, deren Hausnummer "b" ist. Oder wie man auch sagt, der Wert der Variablen, auf die "b" zeigt. Traditioneller Weise stellt man solche Pointer auch als Pfeilchen dar, die ähnlich einem Wegweiser auf das bezeichnete Objekt zeigen.

        b ---> a

In der ersten Anweisung der "Inkrement"-Funktion geben wir den Wert von "*b", also den Wert der Variablen, auf die "b" zeigt, aus. Mit der Anweisung "(*b)++" wird die Variablen, auf die "b" zeigt, um eins erhöht. Die Klammern sind hier notwendig, da das Sternchen schwächer als das "++" bindet und "*b++" eine andere Bedeutung hätte, doch das würde jetzt etwas zu weit führen. In der Zeile darunter geben wir nochmals den Inhalt der Variablen mit Hausnummer "b" aus.

Dem aufmerksamen Leser fällt nun weiterhin auf, dass auch der Aufruf von Inkrement im Hauptprogramm leicht anders aussieht: Dort steht nun

Inkrement(&a)

Das "&" vor der Variablen "a" hat hier allerdings eine andere Bedeutung als die einer Referenz - diese Bedeutung hat "&" nur beim Anlegen einer neuen Variablen wie im Kopf einer Argumentenliste. Stattdessen ermittelt hier "&" die Hausnummer von "a", bzw. technisch korrekt, "&a" ist ein Pointer, der auf "a" zeigt. Genau einen solchen Pointer benötigen wir als Argument von Inkrement().

Kompiliert und startet man nun dieses Programm, so ergibt sich folgende Ausgabe:

a ist 5.
*b ist 5.
*b ist nun 6.
a ist nun 6.

das entspricht ziemlich genau der Ausgabe des ursprünglichen Programmes, nur das wir hier "a" auf eine etwas andere Weise manipuliert haben, nämlich indirekt über den Umweg eines Pointers auf "a".

Kann man auch "b" selbst ausgeben, also die Hausnummer? Aber ja doch!

void Inkrement(int *b)
{
        cout << "*b ist "<<*b<<".\n";
        (*b)++;
        cout << "*b ist nun "<<*b<<".\n";
        cout << "b ist nun "<<b<<".\n";
}

Auf meinem System erzeugt das folgende Ausgabe:

a ist 5.
*b ist 5.
*b ist nun 6.
b ist nun 8985298.
a ist nun 6.

Die Hausnummer von "b" ist hier also "8985298". Falls ihr eine andere Zahl als Hausnummer erhaltet... Keine Panik! Der Rechner vergibt diese Zahlen je nach verfügbarem Speicherplatz immer etwas anders, sogar zwei Abläufe ein und desselben Programmes können hier andere Pointer ergeben. Letztendlich ist das auch relativ irrelevant, brauchen wir doch nur zu wissen, dass in jedem Falle "a" unter dieser Adresse erreichbar ist. Nur wohnt "a" eben jedesmal woanders.

Fensterln mit Intuition: Einstieg in das AmigaOS

Für unsere Aufgabe, einfach ein paar Striche und Punkte auf den Bildschirm zu malen, brauchen wir die Hilfe des Betriebssystems, genauer, zweier Betriebssystemkomponenten: Die "intuition.library" enthält Funktionen zur Manipulation von Fenstern, wie man sie bei der täglichen Arbeit am Amiga kennt, ferner alle möglichen hilfreichen Funktionen zur Programmierung von Benutzerschnittstellen. Mit Hilfe von "intuition" werden wir uns ein Fenster erstellen lassen, in dem die Linien erscheinen - dieses Objekt heißt "Window". Ferner wollen wir in dem Fenster, oder "Window", Linien und Punkte zeichnen. Diese Aufgabe übernimmt eine andere Betriebssystemkomponente, die sinniger Weise "graphics.library" heißt.

Wir benötigen einmal aus der "intuition.library" die Funktionen "OpenWindowTags", die ein Fenster mit einstellbaren Eigenschaften öffnet, sowie "CloseWindow", welches ein solches Fenster wieder verschwinden lässt. Beide Funktionen arbeiten aber nicht mit "Window"- Objekten selbst, sondern mit "Pointern". "OpenWindowTags()" liefert also nur die "Hausnummer" eines Fensters, "CloseWindow()" erwartet auch nur eine solche Hausnummer. Das ist nicht von Nachteil, wir kommen ja mittels eines voran gestellten Sternchens "*" wie oben gesehen an das eigentliche "Window" heran.

Ferner brauchen wir "SetAPen", "WritePixel", "Move" und "Draw" aus der "graphics.library". Mit diesen Funktionen wählt man die Zeichenfarbe, setzt ein einzigen Punkt, bewegt die Schreibposition und zeichnet eine Linie von der letzten Schreibposition zu einer weiteren Koordinate. Diese Funktionen wollen nun allerdings selbst keine "Fenster"-Objekte, sondern benötigen als Parameter einen Pointer auf eine Zeichenfläche, einen sogenannten "RastPort". Die Zeichenfläche eines Fensters befindet sich zum Glück leicht zu finden in der Komponente "RPort" des Fensters, genauer: Ein Pointer auf die Zeichenfläche.

Das klingt erst einmal etwas umständlich... Man hat es deshalb so seltsam eingerichtet, weil es noch andere Objekte außer Windows gibt, die eine Zeichenfläche, einen "RastPort", besitzen und in die man also auch Zeichnen kann.

Zu guter letzt müssen wir dem Compiler das alles noch beibringen:

Das "Window"-Objekt wird in der Datei "" er- klärt, der Rastport in "". Die dazu notwendigen Funktionen zur Manipulation von Fenstern und Zeichenflächen finden sich in "" und "". Wo welche Objekte und Funktionen zu finden sind, muss man entweder wissen, oder in der Betriebssystemdokumentation nachlesen; diese findet sich etwa auf der "Developer CD"

Ferner möchte man dem C++-Compiler noch mitteilen, dass es sich hierbei alles um C-Konstrukte handelt; dazu dient das extern "C" in dem folgenden Programmsegment:

extern "C" {
#include 
#include 
#include 
#include 
}

class Fenster {
        Window *window;         // Das Intuition-Fenster davon
        int breite,hoehe;       // Dimensionen
public:
        Fenster(int b,int h)
        : window(NULL), breite(b), hoehe(h)
                {
                window = OpenWindowTags(NULL,   // dieses Argument brauchen wir nicht.
                        WA_GimmeZeroZero,TRUE,  // Ursprung ist im Zeichenbereich
                        WA_SmartRefresh,TRUE,   // Automatisch wiederherstellen wenn verdeckt
                        WA_InnerWidth,b,        // Breite der Zeichenfläche
                        WA_InnerHeight,h,       // Höhe der Zeichenfläche
                        WA_CloseGadget,TRUE,    // Fenster hat Schließknopf
                        WA_DragBar,TRUE,        // Fenster kann verschoben werden
                        WA_DepthGadget,TRUE,    // Fenster kann nach vorne/hinten gebracht werden
                        WA_IDCMP,IDCMP_CLOSEWINDOW, // reagiert auf was? Fensterschließen!
                        TAG_DONE);
                }
        //
        // Der Destructor löscht auch wieder das Fenster davon, falls offen.
        ~Fenster(void)
        {
                if (window) {
                        CloseWindow(window);
                }
        }
        //
        // Liefere die Zeichenfläche des Fensters. Dies nennt sich RastPort
        RastPort *RastPort_Hiervon(void) const
        {
                return window->RPort;
        }
        //
        // Warte, bis der Benutzer das Close-Gadget drückt.
        void WarteAufSchliessen(void) const
        {
                struct Message *msg;

                msg = WaitPort(window->UserPort);
                msg = GetMsg(window->UserPort);
                ReplyMsg(msg);
        }
};

Die hier definierte Klasse "Fenster" hat als wichtige Komponenten einerseits einen Pointer auf das Betriebssystemobjekt "window", ferner eine Breite und Höhe. Eigentlich könnte man diese auch aus dem "window" selbst auslesen. Der Konstruktor eines "Fensters" bekommt ergo eine Breite und Höhe übergeben, und ruft die "OpenWindowTags()"-Funktion von "intuition" auf, um ein Fenster zu erzeugen. Resultat ist ein Zeiger auf ein "window", welches wir in unserem darüber aufgesetzten "Fenster"-Objekt aufheben. Der Destruktor des "Fenster"- Objektes muss nun auch noch das "window" löschen, was mit "CloseWindow()" funktioniert. Da das OS wie gesagt in "C" statt in "C++" geschrieben ist, kennt Intuition keine "Destruktoren" und wir müssen "windows" deshalb wie hier von Hand löschen. Genau genommen ist es die Aufgabe des "Fenster"- Objektes, diese Arbeit nach außen hin zu verbergen. "RastPort_Hiervon" liefert einen Pointer auf die Zeichenfläche des Fensters, in der die "RPort"-Komponente des Windows zu finden ist. Hierbei tritt das erste Mal der Pfeilchen-Operator "->" auf:

        window->RPort   ist das gleiche wie (*window).RPort

Entziffert man einmal die rechte Seite, so handelt es sich hierbei um: (*window), also das Objekt selbst, auf das der Pointer zeigt. Der "Punkt" bedeutete "Komponente von", ergo: Die Komponente "RPort" des Objektes, auf das "window" zeigt. Da der Punkt stärker bindet als der Stern, muss man noch klammern. Da schreibt sich "window->RPort" einfach flüssiger, und bringt auch graphisch das zum Ausdruck, was passiert: Von "window" aus immer den Pfeilen nach!

Die Methode "WarteAufSchliessen" bedarf noch einiger Erläuterungen: Wir haben bei "OpenWindowTags()" mittels der Argumente "WA_CloseGadget,TRUE" für das zu erstellende Fenster einen Schließknopf angefordert. Nun verhält es sich aber so, dass "intuition" bei Betätigen dieses Knopfes nicht das Fenster löscht. Das wäre sogar fatal, wenn das Programm gleichzeitig versuchen würde, in das dann nicht mehr existente Fenster hinein zu malen. Die Pixel würden ins Nirvana stürzen, und der Rechner gleich hinterher! Statt dessen müssen wir "intuition" auch noch sagen, dass wir gerne einen "Brief" bekommen wollen, wenn der Benutzer das Fenster geschlossen haben will: Dies macht das Argument "WA_IDCMP,IDCMP_CLOSEWINDOW" von "OpenWindowTags()". Die Aufgabe von "WarteAufSchliessen" ist nun, solange zu warten, bis im Briefkasten des Fensters ein Brief eingeht. Der Briefkasten heißt hier "UserPort", ist ein Objekt vom Typ "MsgPort", kurz für "Message Port", und wird bedient mit den Funktionen "WaitPort()", "GetMsg()" und "ReplyMsg()". Alle drei finden sich in der "exec.library", deren Funktionen in der noch nicht erklärten Include-Datei "proto/exec.h" deklariert werden. Genauer möchte ich an dieser Stelle auf die Innereien des Intuition-Nachrichtensytems und der Exec-Bibliothek nicht eingehen; betrachten wir diese Funktion erst mal als schwarzen Kasten - schließlich ist das ein "C++"-Kurs und kein Exec/Intuition-Kurs.

Kommen wir zum Rest des Programmes, der die in der letzten Folge eingeführten Objekte "Punkt" und "Linie" ein wenig ausbaut; diesen Teil bitte an das obige Codesegment anhängen:

class Punkt {
        RastPort *rp;                   // wo zu zeichnen?
        int x;               // X-Koordinate
        int y;               // Y-Koordinate
        int farbe;           // Die Zeichenfarbe
public:
        Punkt(Fenster &fenster,int horizontal,int vertikal,int f = 1)
        {
                x     = horizontal;
                y     = vertikal;
                farbe = f;
                rp    = fenster.RastPort_Hiervon();
        }
        //
        // der Copy-Constructor des Punktes
        Punkt(const Punkt &original)
        {
                x     = original.x;
                y     = original.y;
                farbe = original.farbe;
                rp    = original.rp;
        }
        //
        // Löschen des Punktes: Nichts zu tun
        ~Punkt(void)
        {
        }
        //
        // Einen Punkt zeichen
        void Zeichne(void) const
        {
                SetAPen(rp,farbe);  // definiere die Farbe
                WritePixel(rp,x,y); // das malt den Punkt
        }
        //
        int X_Hiervon(void) const // verrate die X-Komponente
        {
                return x;
        }
        //
        int Y_Hiervon(void) const       // verrate die Y-Komponente
        {
                return y;
        }
        //
        int Farbe_Hiervon(void) const // verrate die Farbe
        {
                return farbe;
        }
        //
        RastPort *RastPort_Hiervon(void) const
        {
                return rp;
        }
        //
};


class Linie {
        RastPort *rp;
        Punkt   anfang;
        Punkt   ende;
public:
        Linie(Punkt &von, Punkt &bis)
         :      rp(von.RastPort_Hiervon()),
                anfang(von),
                ende(bis)
        {
        }
        //
        ~Linie(void)
        {
        }
        //
        void Zeichne(void)
        {
                SetAPen(rp,anfang.Farbe_Hiervon()); // setze den Farbstift
                Move(rp,anfang.X_Hiervon(),anfang.Y_Hiervon());
                Draw(rp,ende.X_Hiervon(),ende.Y_Hiervon());
        }
};

Ein Punkt hat jetzt noch zusätzlich eine Farbe und einen Zeiger auf die Zeichenfläche erhalten, auf der er erscheinen soll, und die Ausgaben auf "cout" der letzen Folge sind verschwunden. Statt dessen gibt es eine neue Methode "Zeichne", die den Punkt auch wirklich erscheinen lässt. Dazu wird in der Zeichenfläche "RastPort" erst einmal der Farbstift "farbe" ausgewählt; dies ist Aufgabe der Betriebssystemfunktion "SetAPen()". Danach wird mittels "WritePixel()" der Punkt an der entsprechende Koordinate gemalt. "RastPort_Hiervon()" ist auch neu und extrahiert die Zeichenfläche des Punktes; das "const" dahinter besagt, dass das Objekt selbst dadurch nicht verändert wird - logo. Woher weiß der Punkt die Zeichenfläche, auf der er erscheinen soll? Nun, dazu gibt's den Konstruktor des Punktes, der sich diese Information vom Fenster holt. Bemerkenswert beim Konstruktor des Punktes ist noch die Syntax "int f = 1":

Punkt(Fenster &fenster,int horizontal,int vertikal,int f = 1)

Dies bedeutet, dass das "f" (Farb-) Argument optional ist und nicht angegeben zu werden braucht. Falls es nicht angegeben ist, soll der Compiler den Wert "1" für die Farbe annehmen.

Das zweite Objekt, die Linie, folgt sofort: Auch dieses Objekt hat eine Zeichenfläche, sowie einen Anfangs- und Endpunkt. Der Konstruktor der Linie benötigt nur zwei Punkte, die Zeichenfläche lässt sich die Linie vom Anfangspunkt mitteilen.

Die Syntax des Linien-Konstruktors ist ein klein wenig anders; nicht, weil es nötig wäre, sondern einfach um diese Variation zu demonstrieren: Hinter dem Doppelpunkt folgen die einzelnen Komponenten "rp", "anfang", "ende", und jeweils in Klammern dahinter wie diese Unterobjekte zu konstruieren sind: Der Zeiger auf die Zeichenfläche mit der Zeichenfläche des Anfangspunktes, der Anfangspunkt mit dem Objekt "von", der Endpunkt mit dem Objekt "bis". Der Rumpf des Konstruktors bleibt einfach leer, die ganze Arbeit wird bereits von der Liste hinter dem Doppelpunkt erledigt.

"Zeichne()" zeichnet nun die Linie: Die Farbe kommt vom Anfangspunkt und wird mittels "SetAPen()" innerhalb der Zeichenfläche gewählt. "Move()" definiert die Startkoordinaten der Linie, mittels "Draw()" wird die Linie letztendlich gezogen.

Bleibt noch das Hauptprogramm:

int main(int argc, char **argv)
{
Fenster f(200,200);     // baue ein Fenster der Größe 200x200
Punkt p(f,10,20);       // mach' nen Punkt bei (10,20)!
Linie l(Punkt(f,30,40,2),Punkt(f,50,60));

        p.Zeichne();
        l.Zeichne();

        f.WarteAufSchliessen();
        return 0;
}

Es erzeugt einen Punkt, eine Linie inklusive beider Anfangs- und Endpunkte, zeichnet Punkt und Linie, und wartet auf das Schließen des Fensters. Die Destruktoren von Punkt, Linie und Fenster räumen dann alles weitere ab.

Und demnächst:

Uff, das war eine ganze Menge diesmal! Außerdem gibt es noch einiges zu bemängeln: Erstens ist überhaupt nicht klar, ob "OpenWindowTags()" überhaupt ein Fenster öffnen konnte; wenn dem Rechner der Speicher ausgeht, oder wenn der Bildschirm zu klein oder das Fenster zu groß ist, gibt es "keinen Pointer" zurück. Dies ist ein Problem der richtigen Fehlerbehandlung, um die wir uns gedrückt haben. Korrekte Fehlerbehandlung ist immer eine knifflige Angelegenheit, die wohl durchdacht sein will. "C++" hilft hier ein wenig mit einem zusätzlichen Konstrukt namens "Exceptions", zu deutsch "Ausnahmen". Ferner ist die Objektstruktur von "Punkt" und "Linie" noch nicht ganz so sauber, wie ich sie mir wünschen würde.

Thomas Richter <thor@math.TU-Berlin.DE>

<< >>