Polymorphismus
Inhaltsverzeichnis
Inhalt der Arbeit:
I. VERANSCHAULICHUNG DES POLYMORPHISMUS
Da das Beispiel trivial ist, verzichte ich hier auf nähere Erklärungen zum Sourcecode und be-schränke mich auf die Kommentierung der Ausgaben.
Auf eine Trennung zwischen Klassenschnittstelle und –implementierung wurde auch in der An-gabe verzichtet, dem schließe ich mich an. Es macht auch keinen Sinn, für jede Klasse eine ei-gene Datei anzulegen, es verschlechtert im Gegenteil die Lesbarkeit des sehr kurzen Pro-grammes.
A. Listing
1. „virtual“-Funktionen
// ********************************
// Übung: 8
// Aufgabe: 1a
// Veranschaulichung des Polymorphismus
// Überschriebene Funktion ist "virtual"
#include
class Base
{ public:
virtual void iAm() { cout << "base "; }
};
class Erbe : public Base
{ public:
virtual void iAm() { cout << "erbe "; }
};
class Erbin : public Base
{ public:
virtual void iAm() { cout << "erbin "; }
};
void main()
{ int i; // Dummy
Base aBase;
Erbe derErbe;
Erbin dieErbin;
Base* basePtrOne=&derErbe;
Base* basePtrTwo=&dieErbin;
cout << "Statische Bindung ...\n";
aBase.iAm();
derErbe.iAm();
dieErbin.iAm();
cout << "\n\nDynamische Bindung ... \n";
basePtrOne->iAm();
basePtrTwo->iAm();
cin >> i; // Dummy
}
2. nicht „virtual“-Funktionen
// ********************************
// Übung: 8
// Aufgabe: 1a
// Veranschaulichung des Polymorphismus
// Überschriebene Funktion ist NICHT "virtual"
#include
class Base
{ public:
void iAm() { cout << "base "; }
};
class Erbe : public Base
{ public:
void iAm() { cout << "erbe "; }
};
class Erbin : public Base
{ public:
void iAm() { cout << "erbin "; }
};
void main()
{ int i; // Dummy
Base aBase;
Erbe derErbe;
Erbin dieErbin;
Base* basePtrOne=&derErbe;
Base* basePtrTwo=&dieErbin;
cout << "Statische Bindung ...\n";
aBase.iAm();
derErbe.iAm();
dieErbin.iAm();
cout << "\n\nDynamische Bindung ... \n";
basePtrOne->iAm();
basePtrTwo->iAm();
cin >> i; // Dummy
}
B. Bildschirmausgabe
Statische Bindung ...
base erbe erbin
Dynamische Bindung ...
erbe erbin
Durch die „virtual“-Deklaration wird die Funktion iAm() nicht anhand des Zeigertyps statisch gebunden. Der Compiler erkennt, daß hier noch nicht klar ist, auf welchen Ob-jekttyp tatsächlich referenziert werden wird. Erst zur Laufzeit wird untersucht, welches Objekt „hinter“ dem Zeiger steckt und die zugehörige Funktion aufgerufen.
Statische Bindung ...
base erbe erbin
Dynamische Bindung ...
base base
Bereits zur Übersetzungszeit werden die Funktionsaufrufe aufgelöst anhand der Zei-gertypen. Da die ensprechende Funktion nicht als (mögliche) polymorphe Funktion gekennzeichnet ist, schließt der Compiler diese Möglichkeit von vornherein aus und wählt die Basisklassenfunktion, weil der Zeigertyp dementsprechend ist.
II. SKETCHPAD
Die Klasse Sketchpad beinhaltet eine dynamische Datenstruktur, weshalb der Programmierer selbst u.a. für einen Copy-Konstruktor und Zuweisungsoperator sorgen muß.
Die Klasse befindet sich nach der Implementierung dieser Funktionen in kanonischer Form.
A. Listing
#ifndef sketchpad__H
#define sketchpad__H
//------------------------------------------------------------
// Class Sketchpad
// Simple character-based graphics class
// System of Coordinates:
// y
// +----
// x |
// |
//------------------------------------------------------------
#include
class Sketchpad
{ public:
Sketchpad(int w= 79, int h= 25, char f= '*', char e= ' ');
Sketchpad(Sketchpad& pad);
virtual ~Sketchpad();
Sketchpad& operator=(const Sketchpad& pad);
void display() const;
void clear();
bool pointOnBoard(int x0, int y0) const;
void putPoint(int x0, int y0);
void putLine(int x0, int y0, int x1, int y1);
void setFillChar(char c) { fillChar= c; }
void setEmptyChar(char c) { emptyChar= c;}
int getWidth() const { return width; }
int getHeight() const { return height; }
private:
char fillChar;
char emptyChar;
int width;
int height;
char** page;
};
#endif
// ********************************
// Übung: 8
// Aufgabe: 1b
// Implementierungsdatei der Klasse Sketchpad
#include "sketchpk.h"
Sketchpad::Sketchpad(Sketchpad& pad)
: width(pad.width), height(pad.height),
fillChar(pad.fillChar), emptyChar(pad.emptyChar)
// Copy-Konstruktor legt 1:1 Kopie an.
{ page=new char* [height];
for(int i=0; i
{ page[i]= new char [width+1];
for(int j=0; j
page[i][j]= pad.page[i][j];
page[i][width]='\0';
}
}
Sketchpad& Sketchpad::operator=(const Sketchpad& pad)
// Zuweisungsoperator
{ if(this==&pad) return *this; // Selbstzuweisung abfangen
for(int i=0; i
delete[] page[i];
delete[] page; // Zeilenarray löschen
page=new char* [height];
height=pad.height; // Einzelne Variablen zuweisen
width=pad.width;
fillChar=pad.fillChar;
emptyChar=pad.emptyChar;
for(int i=0; i
{ page[i]= new char [width+1];
for(int j=0; j
page[i][j]= pad.page[i][j]; // Daten kopieren
page[i][width]='\0';
}
return *this;
}
Sketchpad::Sketchpad(int w, int h, char f, char e)
: width(w), height(h), fillChar(f), emptyChar(e)
{ page= new char* [h];
for (int i=0; i
{ page[i]= new char[w+1];
for (int j=0; j
page[i][j]= emptyChar;
page[i][w]='\0';
}
}
Sketchpad::~Sketchpad()
{ for (int i=0; i
delete[] page[i];
delete[] page;
}
void Sketchpad::display() const
{ for (int i=0; i
cout << page[i] << '\n';
}
void Sketchpad::clear()
{ for (int i=0; i
for (int j=0; j
page[i][j]= emptyChar;
}
bool Sketchpad::pointOnBoard(int x0, int y0) const
{ return (((0 <= x0) && (x0 < height)) && ((0 <= y0) && (y0 < width)));
}
void Sketchpad::putPoint(int x0, int y0)
{ if (pointOnBoard(x0,y0)) page[x0][y0]= fillChar;
}
void Sketchpad::putLine(int x0, int y0, int x1, int y1)
// simple line drawing algorithm
{ int dx= 1;
int dy= 1;
int a= x1-x0;
int b= y1-y0;
int xcrit;
int eps= 0;
if (a<0)
{ dx= -1;
a= -a;
}
if (b<0)
{ dy= -1;
b= -b;
}
xcrit= -b + 2*a;
putPoint(x0,y0);
while ((x0 != x1) || (y0 != y1))
{ if (eps <= xcrit)
{ x0 += dx;
eps += 2*b;
}
if ((eps >= a) || (a <=b))
{ y0 += dy;
eps -= 2*a;
}
putPoint(x0,y0);
}
}
// ********************************
// Übung: 8
// Aufgabe: 1b
// Testreiber für Sketchpad-Klasse
#include
#include "sketchpk.h"
void main()
{ int i; // Dummy
Sketchpad meinPad, deinPad; // Zwei Pads anlegen mit Defaultwerten
meinPad.putLine(1,1,20,60); // Linie zeichnen
cout << "meinPad\n";
meinPad.display(); // ausgeben
cout << "deinPad\n";
deinPad.display(); // zweites Pad ist noch leer
deinPad=meinPad; // ... jetzt nicht mehr!
cout << "deinPad\n"; // Test des Zuweisungsoperators
deinPad.display();
Sketchpad seinPad=meinPad; // Neues Objekt anlegen und initialisieren
cout << "seinPad\n"; // Test des Copy-Konstruktors
seinPad.display();
cin >> i;
}
B. Test
Es werden nur die neu implementierten Funktionen getestet, die vorgefertigten Klassenbestand-teile werden als korrekt angenommen.
Ein Testplan erübrigt sich wohl, da nur der Zuweisungsoperator und der Copy-Konstruktor ge-testet werden.
Der Test besteht einfach darin zu zeigen, daß die Funktionen korrekt arbeiten. Um die Bild-schirmausgaben richtig interpretieren zu können, ist es nötig, ihn in Verbindung mit dem Sour-cecode des Testtreibers zu betrachten.
meinPad
**
***
***
***
***
****
***
***
***
***
***
***
***
***
****
***
***
***
***
**
deinPad
deinPad
**
***
***
***
***
****
***
***
***
***
***
***
***
***
****
***
***
***
***
**
seinPad
**
***
***
***
***
****
***
***
***
***
***
***
***
***
****
***
***
***
***
**
C. Kommentar zu den Änderungen
Wie erwähnt wurden lediglich ein Copy-Konstruktor und ein Zuweisungsoperator hinzugefügt, womit die Klasse in die kanonische Form gebracht wurde.
Beide Funktionen ähneln sich sehr, da sich ja, bis auf die Deallokierung beim Zuweisungsope-rator, die Funktionalitäten gleichen.
Eine weitere Änderung stellt auch die Trennung in Schnittstelle und Implementierung dar. Hier-bei ist zu beachten, daß die Default-Werte des Konstruktors in der Schnittstelle anzugeben sind, da sonst der Compiler einen Fehler meldet, daß er keinen Konstruktor ohne Parameter finden kann!)
III. GRAFISCHE FIGUREN
Eine abstrakte Klasse zeichnet sich dadurch aus, daß sie zumindest eine abstrakte Funktion enthält. Das heißt, daß das Vorhandensein dieser Funktion dem Compiler lediglich angezeigt wird, jedoch kein Code darin enthalten ist. Da eine solche Funktion nicht aufgerufen werden darf (ohne Funktion, daher Fehler schwer zu finden), darf auch kein Objekt dieser Klasse ange-legt werden. Um Fehlern dieser Art vorzubeugen wird dem Compiler mit dem Zusatz „=0“ ange-zeigt, daß diese Funktion abstrakt ist.
Von Klassen dieser Art können also lediglich andere Klassen abgeleitet werden. Diese erben dann die abstrakte Funktion und sich auch abstrakte Klassen ohne Nutzen. Erst wenn in der Implementierung der abgeleiteten Klasse die abstrakte Funktion überschrieben wird mit einer „echten“ Funktion, kann die Klasse wie gewohnt verwendet werden.
Der Sinn dieser abstrakten Basisklassen liegt im Aufbau einer klaren Klassenhierarchie mit kla-ren Schnittstellen. Bei gleichartigen Objekten können so durch die „is A“-Beziehung allgemeine Referenzierungen mit Zeigern vom Typ der abstrakten Basisklasse erfolgen und durch den Po-lymorphismus (Stichwort „virtuelle“ Funktionen) doch die dem jeweiligen „echten“ Objekttyp ent-sprechenden (weil in dieser Klasse implementierten) Funktionen aufgerufen werden (Late Bin-ding).
Interessant ist, daß bei Strukturen die Initialisierung der einzelnen Elemente nur im Anwei-sungsteil des Konstruktors erfolgen kann, weil sonst der Compiler meldet, daß das Objekt z.B. derPunkt schon initialisiert wurde, wenn man derPunkt.y initialisieren will!
A. Listing
// ********************************
// Übung: 8
// Aufgabe: 2
// Klasse Shape
#ifndef shape__H
#define shape__H
#include "sketchpk.h"
class Shape
{ public:
Shape() {}
virtual ~Shape() {}
virtual void draw(Sketchpad* sp) const =0;
virtual void move(int dx, int dy)=0;
};
struct Punkt // Struktur eines Punktes
{ int x,y; };
#endif
// ********************************
// Übung: 8
// Aufgabe: 2
// Schnittstelle der Klasse Dot
#ifndef dot__H
#define dot__H
#include "shape.h"
class Dot : public Shape
{ public:
Dot(int x,int y);
virtual ~Dot() {}
virtual void draw(Sketchpad* sp) const;
virtual void move(int dx, int dy);
private:
Punkt derPunkt;
};
#endif
// ********************************
// Übung: 8
// Aufgabe: 2
// Implementierung der Klasse Dot
#include "dot.h"
Dot::Dot(int x,int y)
{ derPunkt.x=x; derPunkt.y=y; } // Initialisierungsliste bei Strukturen
// nicht möglich!
void Dot::draw(Sketchpad* sp) const
{ sp->putPoint(derPunkt.x,derPunkt.y);
}
void Dot::move(int dx, int dy)
{ derPunkt.x+=dx;
derPunkt.y+=dy;
}
// ********************************
// Übung: 8
// Aufgabe: 2
// Schnittstelle der Klasse Line
#ifndef line__H
#define line__H
#include "shape.h"
class Line : public Shape
{ public:
Line(int x1,int y1,int x2,int y2);
virtual ~Line() {}
virtual void draw(Sketchpad* sp) const;
virtual void move(int dx, int dy);
private:
Punkt punktEins, punktZwei;
};
#endif
// ********************************
// Übung: 8
// Aufgabe: 2
// Implementierung der Klasse Line
#include "line.h"
Line::Line(int x1,int y1,int x2,int y2)
{ punktEins.x=x1; punktEins.y=y1;
punktZwei.x=x2; punktZwei.y=y2;
}
void Line::draw(Sketchpad* sp) const
{ sp->putLine(punktEins.x,punktEins.y,punktZwei.x,punktZwei.y);
}
void Line::move(int dx, int dy)
{ punktEins.x+=dx;
punktEins.y+=dy;
punktZwei.x+=dx;
punktZwei.y+=dy;
}
// ********************************
// Übung: 8
// Aufgabe: 2
// Schnittstelle der Klasse Rectangle
#ifndef rectangle__H
#define rectangle__H
#include "shape.h"
class Rectangle : public Shape
{ public:
Rectangle(int x1,int y1,int x4,int y4);
virtual ~Rectangle() {}
virtual void draw(Sketchpad* sp) const;
virtual void move(int dx, int dy);
private: // 1--2
Punkt punktEins, punktVier; // | |
}; // 3--4
#endif
// ********************************
// Übung: 8
// Aufgabe: 2
// Implementierung der Klasse Rectangle
#include "rectangle.h"
Rectangle::Rectangle(int x1,int y1,int x4,int y4)
{ punktEins.x=x1; punktEins.y=y1;
punktVier.x=x4; punktVier.y=y4;
}
void Rectangle::draw(Sketchpad* sp) const
{ sp->putLine(punktEins.x,punktEins.y,punktEins.x,punktVier.y); // 1-2
sp->putLine(punktVier.x,punktEins.y,punktEins.x,punktEins.y); // 3-1
sp->putLine(punktVier.x,punktEins.y,punktVier.x,punktVier.y); // 3-4
sp->putLine(punktEins.x,punktVier.y,punktVier.x,punktVier.y); // 2-4
}
void Rectangle::move(int dx, int dy)
{ punktEins.x+=dx; punktEins.y+=dy;
punktVier.x+=dx; punktVier.y+=dy;
}
// ********************************
// Übung: 8
// Aufgabe: 2
// Test der abstrakten Basisklasse und der von dieser abgeleiteten Klassen
#include
#include "shape.h"
#include "dot.h"
#include "line.h"
#include "rectangle.h"
void main()
{ int i; // Dummy
Sketchpad pad;
Sketchpad* padPtr=&pad;
Dot derPunkt(10,5);
Line dieLinie(2,2,20,20);
Rectangle dasRechteck(2,2,20,20);
derPunkt.draw(&pad);
dieLinie.draw(&pad);
dasRechteck.draw(&pad);
padPtr->display();
derPunkt.move(10,20);
derPunkt.draw(&pad);
dieLinie.move(5,35);
dieLinie.draw(&pad);
dasRechteck.move(0,30);
dasRechteck.draw(&pad);
padPtr->display();
cin >> i; //Dummy
}
B. Testplan, Test und Bildschirmausgabe
Die Funktionen der Klassen werden durch sequentielle Aufrufe getestet. Der Reihe nach wer-den drei Objekte gezeichnet und anschließend die Zeichenfläche ausgegeben. Dann wird noch jedes Objekt verschoben und erneut ausgegeben.
Beim Verschieben wird das alte Objekt nicht gelöscht. Dies ist aufgrund der vorgegebenen Schnittstelle der Funktion „move“ nur mit relativ großem Aufwand möglich und nach Rückspra-che mit Herrn Strasser auch nicht verlangt.
*******************
** *
* * *
* * *
Objekte wurden gezeichnet.
* * *
* * *
* * *
* * *
* * * *
* * *
* * *
* * *
* * *
* * *
* * *
* * *
* * *
* **
*******************
******************* *******************
** * * *
* * * * *
Objekte wurden verschoben und erneut gezeichnet.
* * * * *
* * * * *
* * * * * *
* * * * * *
* * * * * *
* * * * * * *
* * * * * *
* * * * * *
* * * * * *
* * * * * *
* * * * * *
* * * * * *
* * * * * *
* * * * * *
* ** * **
******************* * *******************
*
*
*
*
IV. KOMPLEXE ZEICHENOBJEKTE
Die Klasse „Drawing“ ist eine kanonische Klasse. Es werden keine dynamischen Datenstruktu-ren in der Klasse selbst angelegt, weshalb der Standard-Konstruktor, -Zuweisungsoperator, etc. völlig ausreichend sind.
Die dynamische Bindung wurde bereits beim letzten Beispiel erklärt, da sie ja bei abstrakten Basisklassen zwingend erfolgen muß. Bei der vorliegenden Klasse tritt sie in der Funktion „draw“ auf, da hier je nach Typ des Listenelementes eine andere Funktion zum Abbilden auf der Zeichenfläche aufgerufen werden muß! Um Polymorphismus überhaupt zu ermöglichen, muß eine Referenzierung auf das Objekt erfolgen, weil ja bei direktem Zugriff der Objekttyp ohnehin schon zur Übersetzungszeit feststeht. Es ist daher nicht erforderlich, wie auch Herr Strasser in der Übung ausführte, daß die Objekte mit Zeigern referenziert werden, auch Referenzen erfül-len diesen Zweck.
Das Sketchpad muß von dem Objekt freigegeben werden, daß es benutzt, also „Drawing“. Die Objekte in der Liste werden von der Liste selbst zerstört, sobald der Geltungsbereich dieses Objektes (die Klasse „Drawing“) verlassen wird, in unserem Fall am Ende des Programmen, wenn das System das Objekt „house“ zerstört.
A. Listing
// ********************************
// Übung: 8
// Aufgabe: 3
// Schnittstelle der Klasse Drawing
#ifndef drawing__H
#define drawing__H
#include
#include "sketchpk.h"
#include "shape.h"
typedef List ShapeList;
class Drawing {
public:
Drawing() {}
Drawing(Sketchpad* pad) : sp(pad) {}
virtual ~Drawing() {}
void add(Shape* s);
void draw() const;
Sketchpad* getSketchpad() const { return sp; }
void setSketchpad(Sketchpad* s) { sp= s; }
private:
ShapeList parts;
Sketchpad* sp;
};
#endif
// ********************************
// Übung: 8
// Aufgabe: 3
// Implementierung der Klasse Drawing
#include "drawing.h"
void Drawing::add(Shape* s)
{ parts.add(s); }
void Drawing::draw() const
{ ShapeList::Iterator iter(parts);
iter.reset();
while (!iter.isAtEnd())
{ iter.element()->draw(sp);;
iter++;
}
}
// ********************************
// Übung: 8
// Aufgabe: 3
// Testtreiber zur Klasse Drawing
#include
#include "drawing.h"
#include "dot.h"
#include "line.h"
#include "rectangle.h"
#include "sketchpk.h"
void main()
{ int i; // Dummy
Sketchpad* s=new Sketchpad;
Drawing house(s);
house.add(new Rectangle(7,10,17,40));
house.add(new Line(7,7,7,43));
house.add(new Line(7,7,2,12));
house.add(new Line(7,43,2,38));
house.add(new Line(2,12,2,38));
house.add(new Rectangle(11,16,14,22));
house.add(new Rectangle(10,28,17,35));
house.add(new Dot(14,29));
house.draw();
s->display();
delete s;
cin >> i; // Dummy
}
B. Testplan, Test und Bildschirmausgabe
Als Test wird das auf dem Angabenblatt vorgegebene Haus verwendet. Weitere Test sind nicht nötig, da das Listentemplate als korrekt angenommen wird und die Klasse Drawing in keiner Weise Einfluß auf die Objekttypen, -anzahl etc. hat.
***************************
* *
* *
* *
* *
*************************************
* *
* *
* ******** *
* ******* * * *
* * * * * *
* * * * * *
* ******* ** * *
* * * *
* * * *
*******************************
Die extreme Einfachheit der Klasse „Drawing“ verlangt daher meiner Meinung nach nicht nach weiteren Tests.