|
|
|
Nachdem wir im letzten Kurs Objekte eingeführt haben, die auch wirklich etwas auf dem Bildschirm anstellen, werden wir dieses Mal die Kenntnisse der Objektorientierung weiter ausbauen: Genauer gesagt, diesmal geht es um Vererbung von Eigenschaften, virtuellen Klassen und Zugriffsberechtigungen auf Member der Objekte; ferner machen wir einen Schlenker in das wichtige Kapitel des Programmdesigns, und wir werden uns mit einer Basisstruktur auseinandersetzen, der linearen Liste.
Der Erzählung erster Teil: Das Design
Unter dem Programmdesign versteht man die Philosophie, die dem Aufbau des Programmes zugrunde liegt. Im Hobbybereich werden Programme oft "ohne Design", man sagt auch "ad hoc", geschrieben, so auch das Programm der letzten Folge. Dies führt oft dazu, dass der Quellcode nach einigen Programmversionen recht vermurkst aussieht, so dass es schwer ist, ihn zu erweitern und zu pflegen, und dass man früher oder später über die eigenen Fehler stolpert. Ein kommerzielles Programm muss man oft über viele Programmversionen pflegen, und darum sollte man sich schon vorher überlegen, wie man seine Programmidee umsetzt. C++ ist eine Programmiersprache, die Autoren in dieser konzeptionellen Arbeit unterstützt - ganz im Gegensatz zu anderen Sprachen wie etwa BASIC, in denen man nur zu leicht Spaghetticode produziert. Der Programmfluss ist dann ähnlich leicht zu verfolgen wie das italienische Nudelprodukt auf einem Teller, und entsprechend schwer zu entheddern.
Schauen wir uns unter diesem Gesichtspunkt einmal den Code vom dritten Teil an: Wir haben einerseits eine Klasse für Fenster, die die Zeichenfläche des geplanten Zeichenprogrammes bildet. Das Fenster selbst weiß aber nicht, welche Objekte in ihm leben. Dann gibt es zwei Objekte, von denen das eine ein Punkt- und das andere ein Linienobjekt ist. Ein Punkt hat zwei Koordinaten, eine Zeichenfläche, sprich, "das Fenster", und eine Farbe. Eine Linie hat auch eine Zeichenfläche, und zwei Punkte, von denen wiederum jeder eine Zeichenfläche und eine Farbe hat. Hier gibt's einiges doppelt! Ferner, welche Farbe hat denn nun eine Linie, die des Anfangs- oder die des Endpunktes?
Wie man schon sieht - hier stimmt was nicht!
Formulieren wir also, was wir genau brauchen, und überlegen wir uns dann, wie man das am besten umsetzt. Kurz gesagt: Machen wir ein Design!
| 1) | Ein Fenster sollte darüber informiert sein, welche Objekte in ihm sind.. |
| 2) | Ein grafisches Objekt sollte irgendwie zeichenbar sein. Wie das genau geschieht, darf dem Fenster eigentlich egal sein. |
| 3) | Ein grafisches Objekt sollte eine Farbe haben. |
| 4) | Es muss Möglichkeiten geben, die Koordinaten von Objekten anzugeben. Dazu brauchen wir ein Objekt, dass Koordinaten vermittelt. Wir hatten im dritten Teil dazu einfach nur die "Punkt"-Klasse verwendet, aber eigentlich stimmt hier was nicht: Das grafische Objekt eines einzelnen Punktes hat zwar Koordinaten und kann auch gemalt werden, aber wir bräuchten eigentlich für die Linie nur die Koordinaten, und würden diese Koordinatenpaare dann mit zusätzlichen Eigenschaften ausrüsten, etwa einer Farbe. Ein "zeichenbarer Punkt", sagen wir ein Pixel, besteht dann auch aus einem Koordinatenpaar, und einer Farbe. |
Diese "Designziele" werden wir im folgenden umsetzen: Für Punkt 1) brauchen wir pro Fenster ein "Inhaltsverzeichnis" aller Objekte darin. Wir werden hierzu sogenannte "einfach verkettete Listen" verwenden, damit der Leser auch ein wenig von den Grundlagen der Informatik mitnimmt. Die Objekte werden auf einer solchen Liste aufgereiht wie Perlen auf einer Kette.
Weiterhin benötigen wir noch ein neues Konzept von C++, nämlich die so genannte "Vererbung" oder "Inheritance". Man kann nämlich auf der Grundlage von bereits definierten Klassen weitere, neue Klassen aufbauen, oder wie man auch sagt, "neue Klassen ableiten". Diesen neuen Klassen werden dann die Methoden und Member der "Basisklasse" "vererbt". In unserem Falle könnten wir beispielsweise wie folgt vorgehen: Ein grafisches Objekt hat 1) eine Zeichenfläche, 2) eine Farbe und 3) eine Methode, sich zu zeichnen:
class GraphikObjekt {
RastPort *rp;
int farbe;
public:
GraphikObjekt(RastPort *port,int f = 1)
: rp(port), farbe(f)
{ }
//
void Zeichne(void)
{
// Da wir nicht wissen, welches Objekt das
// hier ist, können wir natürlich auch nichts
// zeichnen....
cout<<"Dieses Objekt ist nicht zeichenbar.\n";
}
}
|
"RastPort" ist wie im dritten Teil erläutert die Zeichenfläche, die vom OS für alle möglichen Grafikoperationen benötigt wird; es ist das Interface der Grafikbibliothek des AmigaOS, das Zeichenoperationen ermöglicht. Wir bekommen immer nur Zeiger darauf, deshalb das Sternchen vor dem "rp". "farbe" ist der Farbstift des jeweiligen Objektes. Der Konstruktor des Objektes muss natürlich die Zeichenfläche und den Farbstift übergeben bekommen, wobei wir hier mit "int f = 1" dem Aufrufer erlauben, Objekte auch ohne Farbstift zu definieren und als Vorgabe dann "Farbe eins" anzunehmen.
Die Methode "Zeichne" ist noch etwas sonderbar: Sie tut - nichts. Kann sie natürlich nicht, denn mit der Klasse "Graphikobjekt" ist ja noch nicht mal klar, welche Sorte von grafischem Objekt wir überhaupt haben wollen. Also warum schreiben wir sie dann? Das wird erst ein Stückchen später klar, also noch etwas Geduld, bitte...
Kommen wir zum zweiten Objekt, einem Punkt. Im Gegensatz zum dritten Teil soll diesmal klar unterschieden werden zwischen einem Koordinatenpaar als solchem
struct Punkt {
int x,y;
//
Punkt(int x0,int y0)
: x(x0), y(y0)
{ }
};
|
und einem farbigen Pixel, das sich an der Stelle eines solchen Koordinatenpaares befindet. Kümmern wir uns zunächst um den Punkt: Er ist hier als "Struktur" und nicht als "Klasse" definiert. Der einzige Unterschied zwischen beiden ist der, dass man auf die Member einer Struktur ohne weiteres "von außen", also nicht nur von Methoden der Klasse aus, darauf zugreifen kann. Alternativ hätte man auch:
class Punkt {
public:
int x,y;
//
Punkt(int x0,int y0)
: x(x0), y(y0)
{ }
};
|
schreiben können, um "x" und "y" öffentlich zu machen.
Die Philosophie - das Design - ist hier die folgende: In C++ sollte man immer dann Strukturen statt Klassen verwenden, wenn die jeweiligen Objekte nur Daten sammeln und nicht genügend Eigenleben haben, um eine eigene Klasse zu rechtfertigen. Das ist natürlich keine klare Definition und bleibt der Willkür des Programmierers überlassen, aber im obigen Fall des Koordiantenpaares "Punkt" sehe ich die Struktur "Punkt" nur als eine Zusammenfassung zweier Zahlen, und nicht als ein "Ding mit Eigenleben" an. Aus diesem Grunde finde ich die obere Möglichkeit, explizit hier "struct" statt "class" zu verwenden, als diejenige an, die meiner Intention näher liegt. Ob "struct" oder "class" ist damit eine Frage des Designs und keine Frage, die innerhalb der C++ Syntax beantwortet werden kann. Wir werden noch viele solche "Designentscheidungen" in dieser Folge kennenlernen.
Familienangelegenheiten: Vererbung von Eigenschaften.
Ebenso wie der Familiennachwuchs die Augen der Großmutter oder die Ohren des Onkels hat, bekommen Klassen auch die Methoden und Member ihrer Basisklasse vererbt. Wir bauen nun auf den beiden obigen Klassen "Punkt" und "GraphikObjekt" ein "Pixel" auf: Dazu "leiten wir die Pixelklasse vom Graphikobjekt ab".
class Pixel : public GraphikObjekt {
struct Punkt position; // Ort des Pixels
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Position.
Pixel(RastPort *port, int f, int x, int y)
: GraphikObjekt(port,f), position(x,y)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Pixel(RastPort *port, int farbe, Punkt &pos)
: GraphikObjekt(port, farbe), position(pos)
{ }
//
// Mach' 'nen Punkt!
void Zeichne(void)
{
// Farbe holen und setzen: Dies ist
// eine Betriebssystemfunktion
SetAPen(rp,farbe);
WritePixel(rp,position.x,position.y);
}
};
|
Dazu natürlich einige Erläuterungen (Vorsicht, das kompiliert noch nicht!):
class Pixel : public GraphikObjekt {
|
Mittels des "public GraphikObjekt" hinter dem Doppelpunkt sagen wir, dass wir "Pixel" von der Klasse "GraphikObjekt" ableiten. "public" beschreibt die Art der Ableitung, es gibt noch "private" und "protected". Wir werden im Folgenden aber nur "public" verwenden, ich möchte auf diese Details hier auch zunächst nicht eingehen. Damit ist jedes Objekt vom Typ "Pixel" automatisch auch ein Objekt vom Typ "GraphikObjekt" und hat als solches automatisch eine Zeichenfläche, bzw. einen Zeiger auf eine solche, sowie eine Farbe. Diese Member treten, wie man sieht, in der Definition der Punktklasse gar nicht mehr explizit auf, sondern stammen vom Elternteil "GraphikObjekt".
Ferner bringt das Pixel noch ein weiteres Member von sich aus mit, nämlich seine Position - das ist ein Objekt vom Typ "Punkt", wie oben definiert. An dieser Stelle darf man sich fragen, warum wir nicht auch "Pixel" vom "Punkt" abgeleitet haben. Auch dies wäre möglich gewesen:
class Pixel : public GraphikObjekt, public Punkt {
|
nebst einigen kleineren zusätzlichen Änderungen, die jetzt nicht sonderlich relevant sind. Die Pixelklasse hätte dann (wie ja in der Biologie eher typisch) zwei Elternteile statt einem. C++ erlaubt dies - man nennt dieses vornehm auch "multiple inheritance", also "mehrfache Vererbung". Ob man nun die obere oder die untere Lösung bevorzugt, ist erneut eine Frage des Designs. In der oben verwendeten Definition "hat ein Pixel ein Zahlenpaar", in der unteren "ist ein Pixel auch ein Zahlenpaar". Von meinem Empfinden her erscheint mir die Position eines Pixels eine Eigenschaft ähnlich seiner Farbe, wo hingegen ein Pixel nicht selbst ein Koordinatenpaar "ist". Man darf durchaus anderer Meinung sein, sollte aber darauf achten, dass das Design so natürlich und intuitiv wie möglich ist - man will ja seine Programme später auch noch verstehen.
Weiter im Text: Die Pixelklasse hat zwei Konstruktoren bekommen, eine, bei der die Position mittels zweier Zahlen, und noch eine, bei der die Position mittels der Punktstruktur angegeben wird. Je nachdem welche Parameter man bei der Erzeugung von Pixelobjekten übergibt, wird entweder der eine oder der andere Konstruktor verwendet. Und wieder ein Fachausdruck für Angeber, der genau das meint: "C++ unterstützt Polymorphie", d.h. gleichnamige Methoden werden auch aufgrund unterschiedlicher Argumente auseinandergehalten. Für den zweiten Fall übergeben wir die Koordinaten als "Referenz" - das wurde das letzte Mal erläutert.
Schauen wir uns den ersten Konstruktor einmal genauer an:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Position.
Pixel(RastPort *port, int f, int x, int y)
: GraphikObjekt(port,f), position(x,y)
{ }
|
hinter dem Doppelpunkt stehen diesmal nicht nur die Namen von Membern, sondern auch ein Typenname: "GraphikObjekt" ist kein Member von "Punkt", sondern der Typ des Elternteils. Diese Basisklasse ist eben nicht als "Member" Teil des Pixels, sondern das Pixel bekommt die Member von "GraphikObjekt" per Vererbung.
Trotz alledem muss zur Konstruktion der abgeleiteten Klasse "Pixel" die Basisklasse auch konstruiert werden, und dies geschieht in C++ über die Angabe des Typennamens der Basisklasse. Das Member "position" wird hingegen klassisch über seinen Konstruktor zusammengebaut.
Beim zweiten Konstruktor sieht das ganz genauso aus:
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Pixel(RastPort *port, int farbe, Punkt &pos)
: GraphikObjekt(port, farbe), position(pos)
{ }
|
Man entsinne sich nur daran, dass ein Objekt automatisch immer einen Copy- Konstruktor mitbekommt, der hier mittels "position(pos)" bemüht wird. Er braucht nicht - aber kann - explizit definiert werden. Dies hatten wir im zweiten Teil dieses Kurses gesehen.
Letztendlich sollen Punkte auch gezeichnet werden:
// Mach' 'nen Punkt!
void Zeichne(void)
{
// Farbe holen und setzen: Dies ist
// eine Betriebssystemfunktion
SetAPen(rp,farbe);
WritePixel(rp,position.x,position.y);
}
|
Dies setzt mittels der Betriebssystemroutine "SetAPen()" den Farbstift innerhalb der Zeichenfläche "rp" auf "farbe" und zeichnet dann mittels einer zweiten OS-Funktion "WritePixel()" das Pixel an der gewünschten Position. Man beachte, dass wir hier auf die Member "rp" und "farbe" einfach so zugreifen können, obwohl sie im Pixel selbst gar nicht definiert wurden - das sind die Eigenschaften, die von der Basisklasse vererbt wurden!
Kleine Probleme mit dem Nachwuchs: Das Zusammenbauen
Wir basteln jetzt aus den oben erstellten Klassen ein erstes kleines Programm zusammen, das zunächst nur ein einzelnes Pixel auf den Schirm zaubert. Linien malen wir gleich darauf.
#include <iostream.h>
extern "C" {
#include <intuition/intuition.h>
#include <graphics/rastport.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
#include <proto/exec.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,
WA_GimmeZeroZero,TRUE, // Ursprung ist im Zeichenbereich
WA_SmartRefresh,TRUE, // Automatisch wiederherstellen wenn verdeckt
WA_InnerWidth,b,
WA_InnerHeight,h,
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,
TAG_DONE);
}
//
// Der Destruktor 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);
}
};
class GraphikObjekt {
RastPort *rp;
int farbe;
public:
GraphikObjekt(RastPort *port,int f = 1)
: rp(port), farbe(f)
{ }
//
void Zeichne(void)
{
// Da wir nicht wissen, welches Objekt das
// hier ist, können wir natürlich auch nichts
// zeichnen....
cout<<"Dieses Objekt ist nicht zeichenbar.\n";
}
};
struct Punkt {
int x,y;
//
Punkt(int x0,int y0)
: x(x0), y(y0)
{ }
};
class Pixel : public GraphikObjekt {
struct Punkt position; // Ort des Pixels
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Position.
Pixel(RastPort *port, int f, int x, int y)
: GraphikObjekt(port,f), position(x,y)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Pixel(RastPort *port, int farbe, Punkt &pos)
: GraphikObjekt(port, farbe), position(pos)
{ }
//
// Mach' 'nen Punkt!
void Zeichne(void)
{
// Farbe holen und setzen: Dies ist
// eine Betriebssystemfunktion
SetAPen(rp,farbe);
WritePixel(rp,position.x,position.y);
}
};
int main(int argc, char **argv)
{
Fenster f(200,200); // baue ein Fenster der Größe 200x200
Pixel p(f.RastPort_Hiervon(),1,50,50);
p.Zeichne();
f.WarteAufSchliessen();
return 0;
}
|
Die Fensterklasse habe ich dabei unverändert vom letzen Teil übernommen, und das Hauptprogramm ist leicht abgewandelt.
Nur, etwas stimmt noch nicht: Bei Versuch, das Programm zu kompilieren, gibt es folgende und ähnliche Fehlermeldungen:
Error: No access to member "rp" of class "GraphikObjekt".
Warum das? Die Klasse "Pixel" versucht auf das Member "rp" von "GraphikObjekt" zurückzugreifen, aber darf dies offenbar nicht. Das liegt daran, dass "rp" ein privates Member von "GraphikObjekt" ist, wie dies bei Klassen immer voreingestellt ist.
Eine Möglichkeit, dies zu beheben wäre etwa die, "rp" innerhalb von Graphik- Objekt als "public" zu deklarieren; leider könnte dann auch jeder andere von außen einsehen. Eine andere Möglichkeit wäre, wie im dritten Teil gesehen, Zugriffsfunktionen auf die Member zu schreiben.
In diesem Falle gibt es aber eine bessere Methode: Es gibt neben "public" und "private" noch eine dritte Zugriffsberechtigung namens "protected", die die entsprechenden Member nur für abgeleitete Klassen, aber nicht von außerhalb zugreifbar macht. Folgendes ist zu modifizieren:
class GraphikObjekt {
protected:
RastPort *rp;
int farbe;
public:
....
|
Das so modifizierte Programm kompiliert und läuft einwandfrei.
Vielleicht ist aber doch noch einiges beklagenswert: Wir haben nämlich für das Pixel eine Farbe angeben müssen, obwohl dies beim GraphikObjekt noch optional war. Man würde meinen, die offensichtliche Lösung dieses Problems wäre das Ändern der Konstruktoren des Pixels wie folgt:
Pixel(RastPort *port, int f = 1, int x, int y)
: GraphikObjekt(port,f), position(x,y)
{ }
|
aber leider widerspricht das der Syntax von C++: Optionale Argumente müssen immer am Ende der Argumentenliste stehen. Es ergeben sich wiederum mehrere Möglichkeiten zur Lösung dieses Problems:
1) Wir sortieren die Argumentenliste um:
Pixel(RastPort *port, int x, int y, int f = 1)
: GraphikObjekt(port,f), position(x,y)
{ }
|
womit dann C++ ausgetrickst würde, wir aber überall sonst im Programm auch darauf achten müssten, bei der Konstruktion des Punktes die Argumente ebenso zu vertauschen - ansonsten würde aus Versehen die y-Position des Punktes nun als Farbe missverstanden werden. Ferner haben wir das Problem, dass die voreingestellte Farbe ja eigentlich die Privatangelegenheit des GraphikObjektes sein sollte, und eben nicht die des Pixels. Klarer Fall von schlechtem Design also!
2) Wir fügen einige Konstruktoren hinzu, die keine Farbe benötigen:
Pixel(RastPort *port, int x, int y)
: GraphikObjekt(port), position(x,y)
{ }
|
Da nun GraphikObjekt ebenso ohne Farbe konstruiert werden kann, entfällt das Argument "f" damit komplett.
Ein weiterer Punkt ist am Pixel noch auszusetzen: Warum setzen wir die Farbe innerhalb der Zeichenfläche, des RastPorts, eigentlich in der "Pixel" Klasse, wo es doch schon Aufgabe der Basisklasse "GraphikObjekt" ist, die Farbe und die Zeichenfläche aufzuheben?
Auch das lässt sich ändern: Das "GraphikObjekt" bekommt eine neue Methode, mittels der seine Farbe im RastPort ausgewählt wird. Da nur abgeleitete Objekte über diese Methode verfügen müssen, wenn sie sich selbst zeichnen, wollen wir sie gar nicht erst öffentlich machen:
class GraphikObjekt {
protected:
RastPort *rp;
int farbe;
//
// Wähle unsere Farbe im RastPort
void SetzeFarbe(void) const
{
SetAPen(rp,farbe);
}
//
...
|
und dementsprechend ändert sich im "Punkt"-Objekt die "Zeichne"-Methode:
// Mach' 'nen Punkt!
void Zeichne(void)
{
SetzeFarbe(); // Bitte die Basisklasse
WritePixel(rp,position.x,position.y);
}
|
An dieser Stelle erkennt man, denke ich, eine grundlegende Designvorschrift: Das "interne" Wissen einer Klasse sollte so wenig wie möglich nach außen dringen. Stattdessen sollte eine Klasse über Methoden verfügen, die die notwendige Funktionalität nach außen tragen. Oder noch anders ausgedrückt, eine Klasse sollte sich dadurch definieren, was sie kann - und nicht dadurch, aus welchen Komponenten sie besteht.
Warum dies? Nun, knapp gesagt, "was man nicht dokumentiert hat, kann man ändern". Bei kleineren Programmen, von Einzelpersonen geschrieben, ist dieser Punkt vielleicht nicht sonderlich relevant, aber sobald Programme etwas größer werden und auch gepflegt - sprich: ergänzt, korrigiert, gewartet - werden müssen, wird man früher oder später Teile der Programmablaufes ändern müssen. Solcherlei Änderungen lassen sich leichter durchführen, wenn sich Programmteile so wenig wie möglich auf die internen Abläufe anderer, eventuell zu ändernder Programmteile verlassen müssen. Bei einem korrekten Design sollte man also so wenig Information aus dem Innenleben einer Klasse nach außen dringen lassen; ein weiteres Schlagwort für Angeber: "Information Hiding"
Teile des AmigaOS sind leider ohne diese Designvorschrift entwickelt worden und hängen teilweise deswegen so sehr von der zugrunde liegenden Hardware ab, dass eine Änderung im Nachhinein sehr schwierig ist. Dies betrifft etwa den Aufbau des gesamten Grafiksystems, der schwer an diesen Fehlentscheidungen krankt und der die Integration von Grafikkarten zu einem Hack-Abenteuer macht. "Hack" deswegen, weil man sich zwangsläufig auf interne Abläufe verlassen muss, die mit der eigentlich zu erreichenden Funktionalität nicht viel zu tun haben.
Punkt, Punkt, Komma, Strich: Der Ausbau des Programmes
Nachdem wir im letzten Teil auch ein Linienobjekt eingeführt hatten, soll das in dieser Folge natürlich nicht fehlen. Im Gegensatz zum letzen Mal wird eine Linie jetzt durch ein "GraphikObjekt" sowie zwei Punkte definiert - und nicht zwei Pixel. Die Linie hat damit die Farbe des "GraphikObjektes", sowie einen Anfangs- und einen Endpunkt.
Sehr viel mehr gibt es zur Linienklasse nicht zu sagen. Damit diesmal auch noch etwas neues passiert, habe ich eine weitere Klasse eingefügt, die Ellipse. Sie hat eine Farbe, vermittelt durch das GraphikObjekt, dann einen Mittelpunkt und zwei Radien. Die Klasse ist zunächst so angelegt, dass man immer beide Radien angeben muss - eventuell findet es der Leser ja reizvoll, die Ellipsenklasse etwas auszubauen und Konstruktoren einzufügen, die nur einen Radius benötigen und somit auch einfach Kreise zeichnen können. Oder, alternativ, vielleicht könnte man auch eine Kreisklasse durch Ableiten von der Ellipsenklasse erzeugen?
#include <iostream.h>
extern "C" {
#include <intuition/intuition.h>
#include <graphics/rastport.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
#include <proto/exec.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,
WA_GimmeZeroZero,TRUE, // Ursprung ist im Zeichenbereich
WA_SmartRefresh,TRUE, // Automatisch wiederherstellen wenn verdeckt
WA_InnerWidth,b,
WA_InnerHeight,h,
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,
TAG_DONE);
}
//
// Der Destruktor 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);
}
};
class GraphikObjekt {
protected:
RastPort *rp;
int farbe;
//
// Wähle unsere Farbe im RastPort
void SetzeFarbe(void) const
{
SetAPen(rp,farbe);
}
//
public:
GraphikObjekt(RastPort *port,int f = 1)
: rp(port), farbe(f)
{ }
//
void Zeichne(void)
{
// Da wir nicht wissen, welches Objekt das
// hier ist, können wir natürlich auch nichts
// zeichnen....
cout<<"Dieses Objekt ist nicht zeichenbar.\n";
}
};
struct Punkt {
int x,y;
//
Punkt(int x0,int y0)
: x(x0), y(y0)
{ }
};
class Pixel : public GraphikObjekt {
struct Punkt position; // Ort des Pixels
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Position.
Pixel(RastPort *port, int f, int x, int y)
: GraphikObjekt(port,f), position(x,y)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Pixel(RastPort *port, int farbe, Punkt &pos)
: GraphikObjekt(port, farbe), position(pos)
{ }
//
// Ein "farbloser" Konstruktor
Pixel(RastPort *port, int x, int y)
: GraphikObjekt(port), position(x,y)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Pixel(RastPort *port, Punkt &pos)
: GraphikObjekt(port), position(pos)
{ }
//
//
// Mach' 'nen Punkt!
void Zeichne(void)
{
SetzeFarbe();
WritePixel(rp,position.x,position.y);
}
};
//
// von Punkten zu Linien:
//
class Linie : public GraphikObjekt {
struct Punkt anfang,ende; // wo beginnt und wo endet die Linie?
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Start/Endpunkte
Linie(RastPort *port, int f, int x0, int y0, int x1, int y1)
: GraphikObjekt(port,f), anfang(x0,y0), ende(x1,y1)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Positionen als Referenzen übergeben
Linie(RastPort *port, int farbe, Punkt &von, Punkt &bis)
: GraphikObjekt(port, farbe), anfang(von), ende(bis)
{ }
//
// Ein "farbloser" Konstruktor
Linie(RastPort *port, int x0, int y0, int x1, int y1)
: GraphikObjekt(port), anfang(x0,y0), ende(x1,y1)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Linie(RastPort *port, Punkt &von, Punkt &bis)
: GraphikObjekt(port), anfang(von), ende(bis)
{ }
//
//
// Mach' 'nen Strich
void Zeichne(void)
{
SetzeFarbe();
Move(rp,anfang.x,anfang.y);
Draw(rp,ende.x,ende.y);
}
};
//
// von Linien zu Ellipsen:
//
class Ellipse : public GraphikObjekt {
struct Punkt mittelpunkt; // wo ist die Ellipse zentriert?
int radiusx,radiusy; // Radien in x- und y-Richtung
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Mittelpunkt, Radien
Ellipse(RastPort *port, int f, int x, int y, int rx, int ry)
: GraphikObjekt(port,f), mittelpunkt(x,y),
radiusx(rx), radiusy(ry)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Ellipse(RastPort *port, int farbe, Punkt &zentrum, int rx, int ry)
: GraphikObjekt(port, farbe), mittelpunkt(zentrum),
radiusx(rx), radiusy(ry)
{ }
//
// Ein "farbloser" Konstruktor
Ellipse(RastPort *port, int x, int y, int rx, int ry)
: GraphikObjekt(port), mittelpunkt(x,y),
radiusx(rx), radiusy(ry)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Ellipse(RastPort *port, Punkt &zentrum, int rx, int ry)
: GraphikObjekt(port), mittelpunkt(zentrum),
radiusx(rx), radiusy(ry)
{ }
//
//
// Mach' 'nen Kreis
void Zeichne(void)
{
SetzeFarbe();
DrawEllipse(rp,mittelpunkt.x,mittelpunkt.y,radiusx,radiusy);
}
};
int main(int argc, char **argv)
{
Fenster f(200,200); // baue ein Fenster der Größe 200x200
Pixel p(f.RastPort_Hiervon(),1,50,50);
Linie l(f.RastPort_Hiervon(),Punkt(30,40),Punkt(50,70));
Ellipse e(f.RastPort_Hiervon(),70,70,20,50);
p.Zeichne();
l.Zeichne();
e.Zeichne();
f.WarteAufSchliessen();
return 0;
}
|
Die Zeile
DrawEllipse(rp,mittelpunkt.x,mittelpunkt.y,radiusx,radiusy);
ist nun der OS-Aufruf zum Zeichnen einer Ellipse - er macht die eigentliche Arbeit für uns. In der Zeile
Linie l(f.RastPort_Hiervon(),Punkt(30,40),Punkt(50,70));
habe ich einmal das Objekt "l" vom Typ "Linie" nicht durch Angabe der Koordinaten konstruiert, sondern - weil ja bekanntlich Abwechslung das Leben bereichert - durch die Konstruktion zweier temporärer "Punkt"-Objekte, die durch Angaben ihrer Koordinaten erzeugt werden. Mit anderen Worten, diese Zeile benutzt den Konstruktor
Linie(RastPort *port, Punkt &von, Punkt &bis)
: GraphikObjekt(port), anfang(von), ende(bis)
{ }
|
der Linienklasse.
Von Knoten und listigen Listen: Die Verknüpfung von Objekten
Nachdem wir nun zunächst einige grundlegende grafische Objekte erzeugt haben, sollen diese nun so verwaltet werden, dass das Fenster immer weiß, welche Objekte sich in ihm befinden. Hierzu werden wir die im letzten Teil eingeführten Pointer verwenden, um eine sogenannte "einfach verkettete Liste" zu bilden. Eine solche "Liste" kann man sich ähnlich wie eine "Schnitzeljagd" vorstellen: Jedes Objekt bekommt einen Pointer - vorzustellen als ein kleines Pfeilchen - mit, das auf das jeweils nächste Objekt verweist.
Da wir lauter grafische Objekte hintereinander aufreihen wollen, bringt man diesen Pointer am besten im "GraphikObjekt" unter:
class GraphikObjekt {
// Der Zeiger auf den Nachfolger dieses Graphikobjektes
GraphikObjekt *nachfolger;
//
protected:
RastPort *rp;
int farbe;
|
Die von GraphikObjekt abgeleiteten Klassen braucht es wirklich nicht zu interessieren, was der Nachfolger eines GraphikObjektes ist, insofern lassen wir diesen Member einmal "private", und nicht "protected".
Ferner müssen wir bei einer Schnitzeljagd auch wissen, wo diese anfangen soll. Nun denn, da die Fensterklasse ja wissen soll, welche Objekte sich in ihr befinden, bringen wir dort am einfachsten einen Pointer auf das erste GraphikObjekt unter. Dies ist der Startpunkt der Schnitzeljagd - oder genauer, ein Pfeilchen auf das erste Objekt.
class Fenster {
Window *window; // Das Intuition-Fenster davon
int breite,hoehe; // Dimensionen
//
// Hier beginnt die Liste aller grafischen Objekte dieses Fensters
GraphikObjekt *erstesobjekt;
//
|
und zu guter Letzt braucht eine Schnitzeljagd auch ein Ziel - man muss ja wissen, wann man das letzte Element dieser Liste erreicht hat und man keine weiteren Objekte erwarten darf.
Hierzu gibt es einen ganz speziellen Pointerwert, den eine Pointervariable haben darf - und das ist 0. In diesem einen Ausnahmefall darf also eine Zahl einem Zeiger zugewiesen werden - dies geht aber nur für die Zahl 0 und sonst keine andere. Üblicherweise wird mit dem C++-Präprozessor - diesen Teil haben wir noch nicht genauer besprochen - ein Makro definiert, welches NULL heißt, aber auch nur die Zahl 0 repräsentiert. Dies geschieht schon automatisch in den C++-#include-Dateien. Im Grunde genommen ist es dann egal, ob man 0 oder NULL schreibt, aber letzteres gibt dem Experten klar zu erkennen, dass man eigentlich einen besonderen Pointer, und nicht die Zahl 0 meint. C++ macht für die Null keine Unterschiede.
Damit die Pfeilchen niemanden in die Irre weisen können, und dies geschieht in C++ bei fehlerhaften Programmen nur zu leicht, gehört es zu einem defensiven Programmierstil, alle solche Zeiger bei der Erzeugung von Objekten lieber auf NULL zu setzen. Dies habe ich stillschweigend beim Konstruktor der Fensterklasse schon mit dem "window"-Zeiger gemacht. Mittels "if (window)" kann man dann abfragen, ob der Pointer irgendwohin zeigt - dann ist die Bedingung wahr - oder ob es der NULL-Pointer ist - dann ist die Bedingung falsch. Die Zeilen
if (window) {
CloseWindow(window);
}
|
im Destruktor der Fensterklasse rufen also nur dann die Betriebssystemroutine "CloseWindow()" auf, falls der window-Zeiger auch wirklich "wohin", nämlich auf eine Window-Struktur zeigt.
Initialisieren wir also die beiden neuen Zeiger in der Fenster- und der GraphikObjekt-Klasse auf NULL:
public:
Fenster(int b,int h)
: window(NULL), breite(b), hoehe(h), erstesobjekt(NULL)
{
|
ist der neue Konstruktor des Fensters und
public:
GraphikObjekt(RastPort *port,int f = 1)
: nachfolger(NULL), rp(port), farbe(f)
{ }
|
der des GraphikObjekt. Der Grund, warum ich einmal den Pointer am Ende, und einmal am Anfang der Member initialisiere, ist folgender: Nach C++ Richtlinien muss ein Compiler sowieso die nach dem ":" stehenden Initialisierungen in der Reihenfolge der Member innerhalb der Klasse umsortieren - die Gründe hierfür kann ich im Augenblick noch nicht klarmachen. Ein guter Compiler sollte eine Warnmeldung erzeugen, wenn die Reihenfolge der Initialisierungen nicht mit der wirklichen Ausführungsreihenfolge übereinstimmt, weil der Programmierer ja eventuell etwas Anderes im Sinn gehabt haben könnte. Schreiben wir also die Initialisierungen gleich in der Reihenfolge auf, in der die Member-Variablen in der Klasse auftreten! Diese seltsame Regel gilt natürlich nur für die Initialisierungsliste nach dem Doppelpunkt, nicht für eventuellen Programmcode in den geschweiften Klammern!
Wenn wir nun versuchen, den wie oben dargestellt modifizierten Programmcode zu kompilieren, gibt es zunächst ein Problem:
Error: Declaration expected.
meldet der Compiler in der Zeile
// Hier beginnt die Liste aller grafischen Objekte...
GraphikObjekt *erstesobjekt;
|
Diese Fehlermeldung ist leider recht unverständlich, und es bedarf einigen Nachdenkens, was damit gemeint sein könnte.
Das Problem hier ist das folgende: Zu dem Zeitpunkt, zu dem der Compiler diese Zeile bearbeitet, ist die "GraphikObjekt"-Klasse noch gar nicht definiert, also können wir auch keine Zeiger auf diese Klasse deklarieren. Würden wir nun einfach die GraphikObjekt-Klasse im Text nach vorne schieben, so wäre dieses Problem gelöst. Gar nicht zu selten braucht man allerdings im hinteren Element einen weiteren Zeiger auf die erste Klasse - in unserem Fall tritt das zum Glück nicht auf - und dann wäre mit einer einfachen Verschiebung niemandem geholfen. C++ hat für diesen Fall aber eine Lösung parat: Man kann dem Compiler schon mal vor der Definition der Fensterklasse einen Hinweis geben, dass man später eine Klasse "GraphikObjekt" einführen wird. Das ist solange erlaubt, wie man nur Zeiger auf diese bislang undefinierte Klasse verwendet. Dies sieht im Quellcode dann so aus:
//
// Vorwärtsdeklaration der GraphikObjekt-Klasse
class GraphikObjekt;
class Fenster {
...
|
Hinter dem Semikolon ist dann erst mal Schluss - für den Compiler ein Hinweis auf "mehr über diese Klasse weiter unten". Klar, dass man mit einer so deklarierten Klasse nicht sehr viel anderes anfangen kann, als gerade mal Pfeilchen - sprich Pointer - auf solche Objekte aufzustellen.
Der so modifizierte Quellcode kompiliert erst mal, aber verwendet die Zeiger natürlich noch nicht - das müssen wir schon selbst tun. Eine der wichtigsten elementaren Operationen für Listen ist das Einfügen eines Elementes in eine Liste: Ist "F" das Fensterobjekt und "g,h" die GraphikObjekte dieses Fensters, so könnte eine Konstellation vor dem Einfügen eines Objektes etwa so veranschaulicht werden:
F --> g --> h
Dies bedeutet, "erstesobjekt" des Fensters zeigt auf das Objekt g, "nachfolger" von g zeigt auf h, und "nachfolger" von "h" bleibt leer - also ist NULL. Wollen wir nun ein Element "i" einfügen, so macht man dies am einfachsten so, indem man zunächst den Nachfolger von "i" auf das bislang erste Listenelement "g" zeigen lässt:
i
|
V
F --> g --> h
und dann in einem zweiten Schritt den "erstesobjekt"-Zeiger des Fensters auf "i" umbiegt. Damit macht die Liste zwar grafisch einen Schlenker, aber alle Elemente sind wieder hübsch aufgereiht:
_ i
/| |
/ V
F g --> h
Dieses Einfügen eines Elementes geschieht in einer Liste, die der Fensterklasse gehört, und sollte dementsprechend auch dort als Methode aufgenommen werden:
// Einfügen eines GraphikObjekt in das Fenster
void ReiheEin(GraphikObjekt *obj)
{
// nachfolger dieses Objektes auf das
// erste Objekt setzen:
obj->nachfolger = erstesobjekt;
// das erste Objekt ist dann dieses hier
erstesobjekt = obj;
}
|
Diese Methode funktioniert sogar dann - der Leser möge einmal scharf nachdenken - wenn die Liste am Anfang leer ist, also "erstesobjekt" noch auf NULL steht. Zur Erinnerung: "->" bedeutet "folge dem Pfeil, dann werte das Member rechts vom Pfeil aus". "obj->nachfolger" ist das gleiche wie "(*obj).nachfolger", siehe den letzten Teil.
Nur leider gibt's wieder zwei Probleme: Erstens, GraphikObjekt ist an dieser Stelle nicht definiert, sondern nur über eine Vorwärtsreferenz deklariert, der Compiler kann also noch gar nicht wissen, dass es eine Member "nachfolger" eines GraphikObjekt gibt. Zweitens ist der auch noch "privat", also darf die Fensterklasse nicht darauf zugreifen - sollte sie aber können!
Das erste Problem ist leicht gelöst: Wir verschieben die Definition dieser Methode nach unten:
// Einfügen eines GraphikObjekt in das Fenster
void ReiheEin(GraphikObjekt *obj);
|
In der Klasse selbst gibt es also wieder nur eine Vorwärtsreferenz auf die noch zu definierten Methode, und direkt über "main" fügen wir die noch fehlende Definition ein:
// Einfügen eines GraphikObjekt in das Fenster
void Fenster::ReiheEin(GraphikObjekt *obj)
{
// nachfolger dieses Objektes auf das
// erste Objekt setzen:
obj->nachfolger = erstesobjekt;
// das erste Objekt ist dann dieses hier
erstesobjekt = obj;
}
|
Das "Fenster::" besagt: Hier kommt eine Methode der Klasse "Fenster". Ohne den "::"-Operator, der auch "Scope-Operator" heißt, würde der Compiler dies als eine globale Funktion interpretieren, die nichts mit dem Fensterobjekt gemein hat.
Fehlt noch die Lösung des zweiten Problems: Wie erlaubt man den Zugriff auf das "nachfolger"-Member der GraphikObjekt-Klasse vom Fenster aus? Eine Möglichkeit wäre wieder, eine Zugriffsfunktion zu bauen, doch diese würde auch allen anderen Objekten einen Zugriff auf diese eigentlich zum Fenster gehörende Liste gestatten. Darum möchte ich Zugriff auf dieses Member exklusiv für die ReiheEin-Methode des Fensters gestatten; dies erklärt man dem C++ Compiler mittels:
class GraphikObjekt {
friend void Fenster::ReiheEin(GraphikObjekt *);
//
// Der Zeiger auf den Nachfolger dieses Graphikobjektes
GraphikObjekt *nachfolger;
|
womit diese eine Funktion mit dem "GraphikObjekt" befreundet wird.
Nach diesen Ausbauarbeiten können wir problemlos die Linien, Punkte, und Ellipsenobjekte in das Fenster einfügen: In "main" sieht das dann so aus:
f.ReiheEin(&p);
f.ReiheEin(&l);
f.ReiheEin(&e);
|
Man entsinne sich, "&" liefert einen Pointer auf das dahinterstehende Objekt.
Das gesamte Programm sieht damit wie folgt aus:
#include <iostream.h>
extern "C" {
#include <intuition/intuition.h>
#include <graphics/rastport.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
#include <proto/exec.h>
}
//
// Vorwärtsdeklaration der GraphikObjekt-Klasse
class GraphikObjekt;
class Fenster {
Window *window; // Das Intuition-Fenster davon
int breite,hoehe; // Dimensionen
//
// Hier beginnt die Liste aller grafischen Objekte dieses Fensters
GraphikObjekt *erstesobjekt;
//
public:
Fenster(int b,int h)
: window(NULL), breite(b), hoehe(h), erstesobjekt(NULL)
{
window = OpenWindowTags(NULL,
WA_GimmeZeroZero,TRUE, // Ursprung ist im Zeichenbereich
WA_SmartRefresh,TRUE, // Automatisch wiederherstellen wenn verdeckt
WA_InnerWidth,b,
WA_InnerHeight,h,
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,
TAG_DONE);
}
//
// Der Destruktor 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);
}
//
// Einfügen eines GraphikObjekt in das Fenster
void ReiheEin(GraphikObjekt *obj);
};
class GraphikObjekt {
friend void Fenster::ReiheEin(GraphikObjekt *);
//
// Der Zeiger auf den Nachfolger dieses Graphikobjektes
GraphikObjekt *nachfolger;
//
protected:
RastPort *rp;
int farbe;
//
// Wähle unsere Farbe im RastPort
void SetzeFarbe(void) const
{
SetAPen(rp,farbe);
}
//
public:
GraphikObjekt(RastPort *port,int f = 1)
: nachfolger(NULL), rp(port), farbe(f)
{ }
//
void Zeichne(void)
{
// Da wir nicht wissen, welches Objekt das
// hier ist, können wir natürlich auch nichts
// zeichnen....
cout<<"Dieses Objekt ist nicht zeichenbar.\n";
}
};
struct Punkt {
int x,y;
//
Punkt(int x0,int y0)
: x(x0), y(y0)
{ }
};
class Pixel : public GraphikObjekt {
struct Punkt position; // Ort des Pixels
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Position.
Pixel(RastPort *port, int f, int x, int y)
: GraphikObjekt(port,f), position(x,y)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Pixel(RastPort *port, int farbe, Punkt &pos)
: GraphikObjekt(port, farbe), position(pos)
{ }
//
// Ein "farbloser" Konstruktor
Pixel(RastPort *port, int x, int y)
: GraphikObjekt(port), position(x,y)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Pixel(RastPort *port, Punkt &pos)
: GraphikObjekt(port), position(pos)
{ }
//
//
// Mach' 'nen Punkt!
void Zeichne(void)
{
SetzeFarbe();
WritePixel(rp,position.x,position.y);
}
};
//
// von Punkten zu Linien:
//
class Linie : public GraphikObjekt {
struct Punkt anfang,ende; // wo beginnt und wo endet die Linie?
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Start/Endpunkte
Linie(RastPort *port, int f, int x0, int y0, int x1, int y1)
: GraphikObjekt(port,f), anfang(x0,y0), ende(x1,y1)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Positionen als Referenzen übergeben
Linie(RastPort *port, int farbe, Punkt &von, Punkt &bis)
: GraphikObjekt(port, farbe), anfang(von), ende(bis)
{ }
//
// Ein "farbloser" Konstruktor
Linie(RastPort *port, int x0, int y0, int x1, int y1)
: GraphikObjekt(port), anfang(x0,y0), ende(x1,y1)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Linie(RastPort *port, Punkt &von, Punkt &bis)
: GraphikObjekt(port), anfang(von), ende(bis)
{ }
//
//
// Mach' 'nen Strich
void Zeichne(void)
{
SetzeFarbe();
Move(rp,anfang.x,anfang.y);
Draw(rp,ende.x,ende.y);
}
};
//
// von Linien zu Ellipsen:
//
class Ellipse : public GraphikObjekt {
struct Punkt mittelpunkt; // wo ist die Ellipse zentriert?
int radiusx,radiusy; // Radien in x- und y-Richtung
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Mittelpunkt, Radien
Ellipse(RastPort *port, int f, int x, int y, int rx, int ry)
: GraphikObjekt(port,f), mittelpunkt(x,y),
radiusx(rx), radiusy(ry)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Ellipse(RastPort *port, int farbe, Punkt &zentrum, int rx, int ry)
: GraphikObjekt(port, farbe), mittelpunkt(zentrum),
radiusx(rx), radiusy(ry)
{ }
//
// Ein "farbloser" Konstruktor
Ellipse(RastPort *port, int x, int y, int rx, int ry)
: GraphikObjekt(port), mittelpunkt(x,y),
radiusx(rx), radiusy(ry)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Ellipse(RastPort *port, Punkt &zentrum, int rx, int ry)
: GraphikObjekt(port), mittelpunkt(zentrum),
radiusx(rx), radiusy(ry)
{ }
//
//
// Mach' 'nen Kreis
void Zeichne(void)
{
SetzeFarbe();
DrawEllipse(rp,mittelpunkt.x,mittelpunkt.y,radiusx,radiusy);
}
};
// Einfügen eines GraphikObjekt in das Fenster
void Fenster::ReiheEin(GraphikObjekt *obj)
{
// nachfolger dieses Objektes auf das
// erste Objekt setzen:
obj->nachfolger = erstesobjekt;
// das erste Objekt ist dann dieses hier
erstesobjekt = obj;
}
int main(int argc, char **argv)
{
Fenster f(200,200); // baue ein Fenster der Größe 200x200
Pixel p(f.RastPort_Hiervon(),1,50,50);
Linie l(f.RastPort_Hiervon(),Punkt(30,40),Punkt(50,70));
Ellipse e(f.RastPort_Hiervon(),70,70,20,50);
f.ReiheEin(&p);
f.ReiheEin(&l);
f.ReiheEin(&e);
p.Zeichne();
l.Zeichne();
e.Zeichne();
f.WarteAufSchliessen();
return 0;
}
|
Virtuelle Methoden, abstrakte Klassen und reale Probleme
Das Anlegen einer linearen Liste mag ja eine interessante Programmieraufgabe sein, aber nur zum Selbstzweck ist diese Übung doch etwas zu aufwendig. Die Liste soll jetzt also auch praktisch verwendet werden:
Da wir nun jedes der zu zeichnenden Objekte in das Fensterobjekt eingehängt haben - es ist ja Teil seiner Objektliste geworden - müssen wir eigentlich die Objekte nicht mehr alle einzeln zeichnen. Stattdessen können wir das dem Fensterobjekt selbst überlassen, da es ja weiß, welche Objekte in ihm stecken. Insofern sollen die drei Zeilen
p.Zeichne();
l.Zeichne();
e.Zeichne();
|
in main() entfallen, und stattdessen durch einen Aufruf einer noch zu schreibenden Methode
f.ZeichneObjekte();
|
ersetzt werden, wobei diese Methode des Fensters eben alle in dem Fenster verwalteten Objekte auf den Schirm zaubern soll. Fügen wir also in der Fensterklasse noch folgende Deklaration ein:
class Fenster {
...
// Einfügen eines GraphikObjekt in das Fenster
void ReiheEin(GraphikObjekt *obj);
//
// Zeichne alle Objekte des Fensters
void ZeichneObjekte(void);
};
|
Da wir innerhalb von ZeichneObjekte() sicherlich auf die GraphikObjekt-Klasse zurückgreifen müssen - diese Klasse beschreibt ja die zu zeichnenden Objekte - können wir die Methode ZeichneObjekte() nicht direkt im Rumpf der Fensterklasse definieren. Dieses Problem hatten wir oben bei "ReiheEin()" bereits schon einmal. Stattdessen fügen wir die Definition von ZeichneObjekte() direkt über der Definition von main() ein:
// Malen aller Objekte des Fensters
void Fenster::ZeichneObjekte(void)
{
GraphikObjekt *objekt;
//
// hole das erste Element der Liste
objekt = erstesobjekt;
// wiederhole das Folgende solange, wie
// wir das Ende der Liste noch nicht
// erreicht haben.
while(objekt) {
// Zeichne dieses Objekt
objekt->Zeichne();
// hole das nächste Objekt
// dieser Liste
objekt = objekt->nachfolger;
}
// das war's.
}
|
Was geschieht hier? Zunächst deklarieren wir einen Zeiger auf ein GraphikObjekt. Dieser Zeiger wird dann initialisiert mit dem Zeiger auf das erste Element der Objektliste, die wir im Fenster im Member "erstesobjekt" aufheben. Sieht diese Liste wie folgt aus:
F-->i-->h-->g
wobei "F" das Fensterobjekt und damit "erstesobjekt" auf "i" zeigt, so ist nach diesem Schritt "objekt" ein Zeiger auf "i". Wir gehen dann in die folgende while()-Liste hinein: Sie führt die Anweisungen im Schleifenrumpf solange aus, wie die Bedingung in den Klammern hinter dem while()-Schlüsselwort erfüllt ist, also - man vergleiche noch mal mit oben - solange "objekt" nicht der NULL-Zeiger ist. Ist er in diesem Beispiel nicht, denn "objekt" zeigt ja auf "i". Innerhalb der Schleife wird dann "i" gezeichnet: "i->Zeichne()", wobei man sich nochmals über die Bedeutung des "->" Pfeilchens klar sein sollte. In der Zeile darunter
"objekt = objekt->nachfolger"
wird der Objektzeiger umgesetzt, nämlich auf das Objekt, auf den der Nachfolgerzeiger des bisherigen Objektes zeigt. Zeigt also "objekt" wie in unserem Beispiel zunächst auf "i", so zeigt es danach auf "h". Die Schleife kehrt zur Abfrage zurück: h ist nicht NULL - also weiter! Danach wird "h" gezeichnet, und der Zeiger weitergeschaltet, und zwar auf "g". "g" ist nicht NULL, also wird die Schleife nochmals ausgeführt: g zeichnen, danach weiterschalten. Der Nachfolger von "g" ist niemand - sein Nachfolgerzeiger steht auf NULL. Also wird jetzt "objekt" auf NULL gesetzt, und die Schleife wird abgebrochen. Damit sind ja nun auch alle Objekte gezeichnet.
Es bleibt noch eine offensichtliche Schwierigkeit: Wir müssen der Methode "ZeichneObjekte" ja noch den Zugriff auf das private Member "nachfolger" von GraphikObjekt ermöglichen! Das ist leicht erledigt:
class GraphikObjekt {
friend void Fenster::ReiheEin(GraphikObjekt *);
friend void Fenster::ZeichneObjekte(void);
//
...
};
|
Ändern wir nun main() wie besprochen ab und kompilieren den Code:
int main(int argc, char **argv)
{
Fenster f(200,200); // baue ein Fenster der Größe 200x200
Pixel p(f.RastPort_Hiervon(),1,50,50);
Linie l(f.RastPort_Hiervon(),Punkt(30,40),Punkt(50,70));
Ellipse e(f.RastPort_Hiervon(),70,70,20,50);
f.ReiheEin(&p);
f.ReiheEin(&l);
f.ReiheEin(&e);
f.ZeichneObjekte();
f.WarteAufSchliessen();
return 0;
}
|
Wird dieses Programm gestartet, so wird man allerdings enttäuscht:
Zunächst erscheint ein leeres Fenster, ganz ohne Objekte. Schließt man das Fenster, erscheint ein Konsolenfenster, in dem nur dreimal der Text: "Dieses Objekt ist nicht zeichenbar." steht. Na nu, belügt uns der Compiler? Die Objekte Punkt, Linie und Ellipse sind doch zeichenbar? Wo kommt also der Text her? Nun, eine kleine Suche im Quelltext zeigt folgendes:
class GraphikObjekt {
...
void Zeichne(void)
{
// Da wir nicht wissen, welches Objekt das
// hier ist, können wir natürlich auch nichts
// zeichnen....
cout<<"Dieses Objekt ist nicht zeichenbar.\n";
}
}
|
Der Code hat also die Methode "Zeichne" des GraphikObjekt aufgerufen, und nicht die der Punkte, Linien oder Ellipsen. Das liegt nun daran, dass innerhalb von Fenster::ZeichneObjekte() wir objekt->Zeichne() aufrufen, und "objekt" ist ein Zeiger auf ein GraphikObjekt. Dass es sich hierbei "in Wirklichkeit" um einen Punkt, eine Linie und eine Ellipse handelt, weiß der Compiler zum Zeitpunkt des Kompilierens nicht! Und genau *hier* ist das Problem! Die Bindung der Methode "Zeichne" an ein Objekt - also die Feststellung, zu welchem Objekt denn nun "Zeichne" gehört, findet zur Kompilationszeit statt! Und das ist falsch - stattdessen wollen wir erreichen, dass dasjenige "Zeichne" aufgerufen wird, das wirklich zu dem jeweiligen Objekt gehört. Die Bindung muss, wie man sagt, zur Laufzeit aufgelöst werden.
Man kann genau das dem C++-Compiler mitteilen, indem man vor die entsprechende Methode der Basisklasse das Schlüsselwort "virtual" setzt. Dies ist ein Hinweis für den Compiler, einen Aufruf dieser Methode erst zur Laufzeit aufzulösen. Ändern wir also
class GraphikObjekt {
...
virtual void Zeichne(void)
{
// Da wir nicht wissen, welches Objekt das
// hier ist, können wir natürlich auch nichts
// zeichnen....
cout<<"Dieses Objekt ist nicht zeichenbar.\n";
}
}
|
Konsequenterweise sollte man nun auch alle Zeichne-Methoden der vom GraphikObjekt abgeleiteten Klassen als "virtuell" deklarieren, aber dies ist nicht notwendig: Das macht der Compiler von selbst.
Kompiliert und startet man dieses Programm, so erhält man wieder den gewünschten Effekt: Ein modernes Kunstwerk, bestehend aus einer Linie, einem Punkt und einem Kreis in einem Fenster.
An dieser Stelle wieder eine Designfrage: Brauchen wir eigentlich eine Zeichne-Methode des GraphikObjekt? Eigentlich nicht, denn dieses Objekt dient ja quasi nur als "Container" für dafür abgeleitete Klassen und "existiert nie wirklich", sondern nur in der Form eines seiner Kinder. Es gibt nun in C++ eine Möglichkeit, genau diesen Willen auszudrücken, nämlich eine Klasse nur als eine reine Interface- Spezifikation zu verstehen, die man erst ableiten muss, um konkret damit arbeiten zu können. Dies geschieht durch folgende Änderung:
class GraphikObjekt {
..
virtual void Zeichne(void) = 0;
//
}
|
Das "=0;" sagt dem Compiler: Diese Methode ist gar nicht da! Versucht man jetzt etwa ein GraphikObjekt in main mit dem Konstruktur
GraphikObjekt g(f.RastPort_Hiervon(),1);
zu erstellen, so meldet der Compiler einen Fehler:
Error: Class "GraphikObjekt" is abstract (pure virtual function "Zeichne").
Die Klasse "GraphikObjekt" ist als sie selbst gar nicht mehr erzeugbar, da die Methode Zeichne() gar nicht da ist! Solche Methoden nennt man "pure virtual", also "rein virtuell", aus verständlichen Gründen, und derartige Klassen heißen "abstrakt".
Das ganze Programm sieht damit so aus:
extern "C" {
#include <intuition/intuition.h>
#include <graphics/rastport.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
#include <proto/exec.h>
}
//
// Vorwärtsdeklaration der GraphikObjekt-Klasse
class GraphikObjekt;
class Fenster {
Window *window; // Das Intuition-Fenster davon
int breite,hoehe; // Dimensionen
//
// Hier beginnt die Liste aller grafischen Objekte dieses Fensters
GraphikObjekt *erstesobjekt;
//
public:
Fenster(int b,int h)
: window(NULL), breite(b), hoehe(h), erstesobjekt(NULL)
{
window = OpenWindowTags(NULL,
WA_GimmeZeroZero,TRUE, // Ursprung ist im Zeichenbereich
WA_SmartRefresh,TRUE, // Automatisch wiederherstellen wenn verdeckt
WA_InnerWidth,b,
WA_InnerHeight,h,
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,
TAG_DONE);
}
//
// Der Destruktor 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);
}
//
// Einfügen eines GraphikObjekt in das Fenster
void ReiheEin(GraphikObjekt *obj);
//
// Zeichne alle Objekte des Fensters
void ZeichneObjekte(void);
};
class GraphikObjekt {
friend void Fenster::ReiheEin(GraphikObjekt *);
friend void Fenster::ZeichneObjekte(void);
//
// Der Zeiger auf den Nachfolger dieses Graphikobjektes
GraphikObjekt *nachfolger;
//
protected:
RastPort *rp;
int farbe;
//
// Wähle unsere Farbe im RastPort
void SetzeFarbe(void) const
{
SetAPen(rp,farbe);
}
//
public:
GraphikObjekt(RastPort *port,int f = 1)
: nachfolger(NULL), rp(port), farbe(f)
{ }
//
virtual void Zeichne(void) = 0;
//
};
struct Punkt {
int x,y;
//
Punkt(int x0,int y0)
: x(x0), y(y0)
{ }
};
class Pixel : public GraphikObjekt {
struct Punkt position; // Ort des Pixels
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Position.
Pixel(RastPort *port, int f, int x, int y)
: GraphikObjekt(port,f), position(x,y)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Pixel(RastPort *port, int farbe, Punkt &pos)
: GraphikObjekt(port, farbe), position(pos)
{ }
//
// Ein "farbloser" Konstruktor
Pixel(RastPort *port, int x, int y)
: GraphikObjekt(port), position(x,y)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Pixel(RastPort *port, Punkt &pos)
: GraphikObjekt(port), position(pos)
{ }
//
//
// Mach' 'nen Punkt!
void Zeichne(void)
{
SetzeFarbe();
WritePixel(rp,position.x,position.y);
}
};
//
// von Punkten zu Linien:
//
class Linie : public GraphikObjekt {
struct Punkt anfang,ende; // wo beginnt und wo endet die Linie?
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Start/Endpunkte
Linie(RastPort *port, int f, int x0, int y0, int x1, int y1)
: GraphikObjekt(port,f), anfang(x0,y0), ende(x1,y1)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Positionen als Referenzen übergeben
Linie(RastPort *port, int farbe, Punkt &von, Punkt &bis)
: GraphikObjekt(port, farbe), anfang(von), ende(bis)
{ }
//
// Ein "farbloser" Konstruktor
Linie(RastPort *port, int x0, int y0, int x1, int y1)
: GraphikObjekt(port), anfang(x0,y0), ende(x1,y1)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Linie(RastPort *port, Punkt &von, Punkt &bis)
: GraphikObjekt(port), anfang(von), ende(bis)
{ }
//
//
// Mach' 'nen Strich
void Zeichne(void)
{
SetzeFarbe();
Move(rp,anfang.x,anfang.y);
Draw(rp,ende.x,ende.y);
}
};
//
// von Linien zu Ellipsen:
//
class Ellipse : public GraphikObjekt {
struct Punkt mittelpunkt; // wo ist die Ellipse zentriert?
int radiusx,radiusy; // Radien in x- und y-Richtung
//
public:
// Konstruktor des Objektes: Zeichenfläche, Farbe,
// Mittelpunkt, Radien
Ellipse(RastPort *port, int f, int x, int y, int rx, int ry)
: GraphikObjekt(port,f), mittelpunkt(x,y),
radiusx(rx), radiusy(ry)
{ }
//
// Noch ein Konstruktor: Dieser bekommt die
// Position als Referenz übergeben
Ellipse(RastPort *port, int farbe, Punkt &zentrum, int rx, int ry)
: GraphikObjekt(port, farbe), mittelpunkt(zentrum),
radiusx(rx), radiusy(ry)
{ }
//
// Ein "farbloser" Konstruktor
Ellipse(RastPort *port, int x, int y, int rx, int ry)
: GraphikObjekt(port), mittelpunkt(x,y),
radiusx(rx), radiusy(ry)
{ }
//
// und noch mal das gleiche, jedoch mit einer
// Punktstruktur
Ellipse(RastPort *port, Punkt &zentrum, int rx, int ry)
: GraphikObjekt(port), mittelpunkt(zentrum),
radiusx(rx), radiusy(ry)
{ }
//
//
// Mach' 'nen Kreis
void Zeichne(void)
{
SetzeFarbe();
DrawEllipse(rp,mittelpunkt.x,mittelpunkt.y,radiusx,radiusy);
}
};
// Einfügen eines GraphikObjekt in das Fenster
void Fenster::ReiheEin(GraphikObjekt *obj)
{
// nachfolger dieses Objektes auf das
// erste Objekt setzen:
obj->nachfolger = erstesobjekt;
// das erste Objekt ist dann dieses hier
erstesobjekt = obj;
}
// Malen aller Objekte des Fensters
void Fenster::ZeichneObjekte(void)
{
GraphikObjekt *objekt;
//
// hole das erste Element der Liste
objekt = erstesobjekt;
// wiederhole das folgende solange wie
// wir das Ende der Liste noch nicht
// erreicht haben.
while(objekt) {
// Zeichne dieses Objekt
objekt->Zeichne();
// hole das nächste Objekt
// dieser Liste
objekt = objekt->nachfolger;
}
// das war's.
}
int main(int argc, char **argv)
{
Fenster f(200,200); // baue ein Fenster der Größe 200x200
Pixel p(f.RastPort_Hiervon(),1,50,50);
Linie l(f.RastPort_Hiervon(),Punkt(30,40),Punkt(50,70));
Ellipse e(f.RastPort_Hiervon(),70,70,20,50);
f.ReiheEin(&p);
f.ReiheEin(&l);
f.ReiheEin(&e);
f.ZeichneObjekte();
f.WarteAufSchliessen();
return 0;
}
|
Halten wir also fest: Mittels des Schlüsselwortes "virtual" erfolgt die Bindung einer Methode an ein Objekt zur Laufzeit, und nicht zur Compilerzeit. Dass dies durchaus etwas Unterschiedliches sein kann, haben wir oben gesehen: Mit "virtual" ruft man diejenige Methode auf, die "wirklich zu dem einzelnen Objekt" gehört, ohne "virtual" diejenige, die zur Klasse gehört, die der Compiler zur Compilezeit feststellen kann.
Warum macht man also nicht gleich alles "virtual", denn damit werden doch offenbar Methoden "gleich richtig" an die wirklichen Objekte gebunden, und nicht an das, was der Compiler für richtig hält"?
Auf diese Frage gibt es zwei Anworten: Erstens, eventuell will man genau das nicht. In bestimmten Anwendungen kann es durchaus sinnvoll sein, eine bestimmte Methode nur zur Compilezeit an das Objekt zu binden. Die zweite Antwort ist eine rein praktische: "virtuelle" Bindungen sind langsamer. Ein Funktionsaufruf einer "virtuellen" Methode dauert - abhängig vom Compiler - ein klein wenig länger. Virtuelle Methoden sind auch schlechter optimierbar. Für normale Anwendungen mag dies unwesentlich sein, für bestimmte zeitkritische Probleme aber sollte man dann auf "virtual" verzichten.
Ein Blick über den Tellerrand: In der mit C++ verwandten Sprache "Java" sind Methoden immer virtuell, es sei denn man deklariert sie als "final". Abstrakte Klassen heißen dort auch "interfaces", was eine etwas treffendere Namengebung ist.
Ausblicke:
Dies war eine ganze Menge Holz für diesen Kurs: Wir haben uns mit Designproblemen beschäftigt, mit linearen Listen und mit virtuellen Methoden vererbter Klassen. Der Leser ist natürlich herzlich dazu aufgefordert, das bisherige Programm etwas aufzubohren und zu ergänzen.
Demnächst auf diesem Kanal: Wir werden uns weiterhin mit einigen Design- Problemchen beschäftigen, insbesondere mit Konstruktoren und Destruktoren abgeleiteter und abstrakter Klassen, mit dynamischer Speicherverwaltung und, letztendlich, mit einem klarer strukturiertem Programmaufbau, mit der Einteilung eines Programmes in Module. Inhaltlich werden wir uns mehr mit dem Nachrichtensystem von Intuition auseinandersetzen, um mit dem Programm auch etwas Praktisches anfangen zu können.
Thomas Richter <thor@math.TU-Berlin.DE>