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 <iostream.h>
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 <iostream.h>
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 <iostream.h>
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 "<intuition/intuition.h>" er-
klärt, der Rastport in "<graphics/rastport.h>". Die dazu notwendigen
Funktionen zur Manipulation von Fenstern und Zeichenflächen finden
sich in "<proto/intuition.h>" und "<proto/graphics.h>". 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 <intuition/intuition.h>
#include <proto/intuition.h>
#include <proto/exec.h>
#include <proto/graphics.h>
}
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>
|