FH Darmstadt
FB Informatik
Prof.Dr.H.P.Weber |
Programmieren II
Übung
|
Juni 2005
|
Es soll die folgende Klassenhierarchie für zweidimensionale Figuren
implementiert werden:
Abstrakte Basisklasse Shape, davon direkt abgeleitet die
Klassen
Polyline und Ellipse. Von
Polyline direkt abgeleitet die Klassen Line
und Polygon und zusätzlich von Polygon
abgeleitet die Klasse Rectangle. Außerdem von Ellipse
abgeleitet die Klasse Circle. |
Zunächst werden nur die grundlegenden Klassen Shape und
Polyline entwickelt und getestet. Da die Klassen der Shape-Hierarchie
mit Punkten arbeiten, wird zur Darstellung eines Punktes auch eine Klasse
Point definiert:
-
Die Klasse Point besitzt die public-Elemente
x und y vom Typ double und einen
entsprechenden Konstruktor mit zwei Parametern, die beide den Default-Wert
0.0 haben.
Die Methode distance liefert den Abstand zu einem zweiten
Punkt, der als Argument übergeben wird. Der Abstand von zwei Punkten (x1, y1)
und (x2, y2) ist die Wurzel aus (x2 - x1)2 + (y2 - y1)2 .
Die Methode toString liefert den Punkt als String in der Form
(x, y).
Der Operator *= multipliziert den Punkt mit einer
Gleitpunktzahl.
Außerdem werden die folgenden globalen Operationen bereitgestellt:
+ und -
Liefert die Summe bzw. Differenz zweier Punkte.
== und != Vergleicht zwei
Punkte.
<<
Gibt einen Punkt auf einen Stream aus.
-
Die Shape-Klasse enthält ein Attribut anchor (Anker,
Bezugspunkt), das die Lage der Figur festlegt. Durch Verschiebung des Ankers
wird also die gesamte Figur verschoben. Der Anker wird in der Shape-Klasse
als protected-Element gespeichert. Der Konstruktor
initialisiert den Anker mit dem als Argument übergebenen Punkt oder mit dem
Default-Wert Point(0,0). Zugriffsmethoden erlauben das Lesen und
Setzen des Ankers. Die Verschiebung erfolgt mit der Methode move,
die zwei Parameter für die Verschiebung in x- und y-Richtung besitzt.
-
Die Klasse Shape ist eine abstrakte Klasse. Neben dem virtuellen
Destruktor stellt sie das folgende polymorphe Interface bereit: Die Methode toString
liefert die Daten einer Figur als String. Für die Klasse Shape
sind das die Koordinaten des Ankers. Außerdem enthält Shape
die rein virtuellen Methoden scale und draw. scale
verkleinert oder vergrößert die Figur um einen Faktor, der als Argumnet
übergeben wird. Die Methode draw wird zum Zeichnen einer Figur
bereitgestellt, aber nicht genutzt (d.h. leer implementiert).
-
Die Klasse Polyline stellt einen Linienzug dar. Beispielsweise
besteht folgender Linienzug : Anker - E1 - E2 - E3 - E4 aus vier Linien mit den Endpunkten E1 bis E4. Die Endpunkte werden relativ zum
Anker in einem dynamisch erzeugten Array gespeichert. Entsprechend besitzt die
Klasse Polyline zwei Datenelemente: Ein dynamisches Element,
nämlich einen Zeiger auf das Array mit Point-Elementen, und
eine Variable für die Anzahl der Linien. Definieren Sie verschiedene
Konstruktoren: Einen Default-Konstruktor, einen Konstruktor mit einem Point-Parameter
zur Festlegung des Ankers, einen Konstruktor mit zwei Point-Parametern
für eine Linie und einen Konstruktor, dem ein Array mit den Punkten eines
Linienzuges und die Anzahl der Punkte übergeben werden. Jeder Konstruktor setzt
den Zeiger und die Anzahl der Linien auf 0, wenn das Objekt noch
keine Linie enthält. Andernfalls erzeugt der Konstruktor dynamisch das Array für
die Endpunkte gemäß der aktuellen Anzahl Linien. Für jeden Endpunkt wird
die Differenz zum Anker gespeichert. Das dynamische Array wird vom Destruktor
wieder freigegeben. Da die Klasse Polyline ein dynamisches
Element besitzt müssen auch Kopierkonstruktor und Zuweisungsoperator definiert
werden.
-
Redefinieren Sie für die Klasse Polyline alle rein virtuellen
Methoden der Klasse Shape und die Methode toString, die
die Koordinaten der Eckpunkte als String liefert.
Stellen Sie ferner eine Methode bereit, die die Anzahl Linien im Polygon
zurückgibt, und eine Methode, die die Gesamtlänge liefert.
-
Überladen Sie schließlich zweimal den operator+=: Ist das
Argument ein Punkt, soll eine weitere Linie zu diesem Punkt angehängt werden.
Ist das Argument ein Polyline-Objekt, soll dessen gesamter
Linienzug an den letzten Punkt angehängt werden.
-
Wenn eine Linie angehängt wird, muss das Array mit den Endpunkten vergrößert
werden.
-
Zu den relativen Koordinaten des anzuhängenden Linienzugs müssen die relativen
Koordinaten des letzten Punktes addiert werde.
Testen Sie die Klasse Polyline, indem Sie mit jedem zur
Verfügung stehenden Konstruktor ein Objekt erzeugen und sich anzeigen lassen.
Rufen Sie dann jede Methode der Klasse mindestens einmal auf.
Ergänzen Sie die Shape-Klassenhierarchie um die noch
fehlenden Klassen Line, Polygon, Rectangle,
Ellipse und Circle:
-
Die Klasse Line besitzt mindestens zwei Konstruktoren: Die
Endpunkte einer Linie sollen als zwei Point-Objekte oder direkt
in x-, y-Koordinaten angegeben werden können. Die Redefinition von geerbten
Methoden ist nicht notwendig. Allerdings soll es nicht möglich sein, an eine
Linie einen weiteren Punkt oder einen Linienzug anzuhängen!
-
Ein Polygon wird als geschlossener Linienzug betrachtet: Die Linie vom letzten
Punkt zum ersten Punkt (=Anker) gehört logisch zur Figur. Entsprechend müssen
die Methoden redefiniert werden, die die Anzahl Linien bzw. die Gesamtlänge
zurückgeben. Stellen Sie zusätzlich Methoden bereit, die die Anzahl Ecken (=
Anzahl Linien) und den Umfang (=Gesamtlänge) liefern:
-
Die Klasse Rectangle beschreibt Vierecke mit der linken unteren
Ecke als Anker, deren Seiten parallel zu den x- und y-Koordinaten verlaufen.
Definieren Sie zuerst einen Konstruktor, dem der Anker sowie die Breite und
Höhe des Rechtecks übergeben wird. Ein zweiter Konstruktor erzeugt ein
Rechteck aus zwei Punkten, nämlich der linken unteren Ecke und der rechten
oberen Ecke. Stellen Sie Methoden bereit, die die Höhe und Breite des Rechtecks
liefern. Auch für Rechtecke soll es nicht möglich sein, einen weiteren Punkt
oder einen Linienzug anzuhängen.
-
Definieren Sie schließlich die Klassen Ellipse und Circle.
Eine Ellipse wird durch den Mittelpunkt und die beiden Halbachsen a und b
beschrieben. Der Mittelpunkt ist der Anker der Figur. Neben einem Konstruktor
und den Zugriffsmethoden für die Halbachsen soll auch eine Methode definiert
werden, die den Umfang der Ellipse liefert (Verwenden Sie zur Berechnung des
Umfangs einer Ellipse die Näherungsformel U = PI * ( 3/2*(a+b) - sqrt(a*b) )
wobei PI = 3.14159). Außerdem müssen die geerbten Methoden scale
und toString redefiniert werden.
-
Ein Kreis ist eine Ellipse, deren Halbachsen gleich lang sind und den Radius des
Kreises bilden. Außer einem Konstruktor soll die Klasse Circle
zusätzlich die Methoden getRadius und setRadius
bereitstellen. Auch ist die Methode toString zu redefinieren.
-
Erweitern Sie das Anwendungsprogramm aus Teil 1 so, dass auch die neuen Klassen
getestet werden.
Die Figuren eines 'Bildes' bestehend aus Objekten der Shape-Hierarchie
werden in einer verketteten Liste verwaltet und in der Reihenfolge 'gezeichnet',
wie sie in der Liste vorkommen. Dazu soll std::list aus der STL
verwendet werden:
-
Zur Realisierung einer inhomogenen Liste werden nicht die Objekte selbst sondern
Shape-Zeiger in der Liste gespeichert. Dem Listentyp soll
daher der Name ShapePtrList mittels typedef
zugewiesen werden. Danach können Objekte dieses Typs angelegt und die in std::list
vorhandenen Methoden aufgerufen werden, z.B. wird mit push_back
ein Shape* auf ein zuvor erzeugtes Objekt der Shape-Hierarchie
an die Liste angehängt.
-
Schreiben Sie ein Anwendungsprogramm, das Zeiger auf verschiedene Objekte der Shape-Hierarchie
in eine Liste vom Typ ShapePtrList einfügt. Die Objekte sollen
mit new dynamisch erzeugt werden. Geben Sie die Shape-Objekte
der Liste aus. Löschen Sie dann einige Elemente und zeigen Sie die Liste erneut
an. Schreiben Sie zur Ausgabe der Shape-Objekte eine globale
Funktion printShapeList, die als Argument eine Referenz auf die
Liste erhält und mithilfe der Methoden toString
die Shape-Objekte der Liste anzeigt. Geben Sie zusätzlich mit
dem Operator typeid auch den Typ der Objekte
aus.
Die Klasse ShapePtrList wird noch in zwei Schritten
verbessert:
-
Bisher speichert jedes Listenelement einen einfachen Shape-Zeiger.
Deshalb wird beim Löschen eines Listenelements nur der Zeiger nicht aber das Shape-Objekt
selbst gelöscht, was zu Speicherlecks führt. Ebenso werden beim Kopieren und
Zuweisen ganzer Listen nur die Zeiger kopiert ('flache Kopie'), was gravierende
Laufzeitfehler verursachen kann. Daher sollen die Zeiger durch 'intelligente
Zeiger (smart pointer)' ersetzt werden, die z.B. das adressierte Objekt
zerstören, wenn sie selbst zerstört werden. Gehen Sie dazu wie folgt vor:
-
Ergänzen Sie zuerst die Shape-Klasse durch eine rein virtuelle
Methode clone. Diese Methode muss in jeder abgeleiteten Klasse
redefiniert werden, indem sie eine dynamisch erzeugte Kopie des aktuellen
Objekts zurückgibt.
-
Definieren Sie dann die Klasse ShapePtr zur Darstellung eines
intelligenten Zeigers auf Shape-Objekte. Als Attribut besitzt
die Klasse einen Shape-Zeiger, der vom Konstruktor mit dem
Parameter vom Typ Shape* bzw. dem Default-Wert 0 initialisiert
wird. Außerdem muss ein eigener Kopierkonstruktor definiert werden. Dieser
erzeugt zunächst mithilfe der Methode clone eine Kopie des Shape-Objekts
und läßt den Shape-Zeiger auf diese Kopie zeigen. Der
Destruktor zerstört das Shape-Objekt, auf das der Zeiger
verweist. Der Zuweisungsoperator wird zweifach überladen, so dass sowohl ein
anderes ShapePtr-Objekt als auch ein einfacher Shape-Zeiger
zugewiesen werden kann. In beiden Fällen wird das aktuelle Shape-Objekt
zerstört und der Zeiger auf eine Kopie des Shape-Objekts
gesetzt, auf das der übergebene Zeiger verweist. Die Operatoren *
und -> sind so zu überladen, dass ein ShapePtr-Objekt
wie ein gewöhnlicher Zeiger verwendet werden kann. Der
Dereferenzierungsoperator liefert eine Referenz auf das adressierte Shape-Objekt
und der Pfeiloperator liefert den Shape-Zeiger selbst.
Schließlich soll auch eine Konvertierung eines ShapePtr-Objekts
in einen Shape-Zeiger möglich sein. Fügen Sie deshalb noch die
entsprechende Konvertierungsfunktion hinzu.
Die Definition der Liste mit Zeiger auf Shape-Objekte lautet nun
wie folgt:
typedef List< ShapePtr > ShapePtrList;
Stellen Sie die Definitionen der Klassen ShapePtr und ShapePtrList
in eine neue Header-Datei ShapeList.h. Verwenden Sie zum Testen
die Funktionen main und printShapeList aus Teil
3. Diese sollten unverändert mit der neuen Definition der Liste lauffähig
sein.
-
Um die Klasse ShapePtrList mit eigenen Methoden erweitern zu
können, wird jetzt die Klasse ShapePtrList von der
Standardklasse std::list abgeleitet:
class ShapePtrList : public list< ShapePtr >
Ergänzen Sie die Klasse ShapePtrList durch die
Methoden scale und toString. Die Methode scale
verkleinert oder vergrößert jede Figur der Liste um einen Faktor, der als
Argument übergeben wird. Die Methode toString liefert einen
String mit dem Inhalt der Liste: Für jede Figur der Liste wird in einer neuen
Zeile zuerst der Typname der Figur in den String geschrieben und dann das
Ergebnis der Aufrufes der toString-Methode für die jeweilige
Figur.
Testen Sie die verbesserte Klasse
ShapePtrList. Ändern Sie die main-Funktion
aus Teil 3 wie folgt: Entfernen Sie die globale Funktion printShapeList
und verwenden Sie zur Ausgabe der Liste die Methode toString.
Erzeugen Sie eine Kopie der Liste und modifizieren Sie die Figuren der neuen
Liste durch einen Aufruf der Methode scale. Nur die Figuren der
neuen Liste dürfen sich dadurch verändern. Testen Sie auch die Zuweisung
ganzer Listen. Den Aufruf der Destruktoren können Sie sichtbar machen,
indem Sie im virtuellen Destruktor der Klasse Shape eine
Meldung ausgeben.
Quelle: Prinz / Kirch-Prinz: C++ Das Übungsbuch
|