Abstract classes

Αφηρημένες κλάσεις

Αφηρημένες κλάσεις χρησιμεύουν ως οργανωτικά πρότυπα από τα οποία κατασκευάζουμε υπο-κλάσεις. Εδώ ορίζω την αφηρημένη κλάση shape (σχήμα) από την οποία παράγω τις υποκλάσεις line και circle (ευθεία και κύκλος). Αφηρημένες κλάσεις όπως η shape δεν επιτρέπουν κατασκευή αντικειμένων αυτού του τύπου. Απλά χρησιμοποιούνται συγκεντρώνοντας ορισμένες κοινές λειτουργίες υποκλάσεών τους. Έτσι μετά τον ορισμό των κλάσεων αυτών, δεν μπορώ να γραψω μιά εντολή της μορφής:
 shape  myShape;
Μιά τέτοια εντολή προξενεί μήνυμα λάθους από τον compiler. Μιά κλάση χαρακτηρίζεται αφηρημένη όταν έχει μιά τουλάχιστον virtual συνάρτηση με μηδενικό σώμα, όπως π.χ.
 	virtual void printEq() const = 0; 
virtual (εικονική) σημαίνει ότι κάθε υποκλάση που θα δημιουργηθεί, πρέπει να ορίσει την δική της συνάρτηση αυτού του τύπου. Η συγκεκριμένη συνάρτηση είναι αυτή που μου τυπώνει την εξίσωση του σχήματος. π.χ. γιά μιά ευθεία line τυπώνει κάτι σαν το:
 aX + bY + c = 0
ενώ γιά έναν κύκλο circle τυπώνει κάτι σαν το
 X2 + Y2 + aX + bY + c = 0 .
Στο σύστημα λοιπόν των τριών κλάσεων που θα φτιάξω, θα υπάρχουν τρεις συναρτήσεις της ίδιας μορφής, αλλά με διαφορετικό σώμα η κάθε μιά:
virtual void printEq() const = 0;     χωρίς σώμα στην αφηρημένη κλάση shape
virtual void printEq() const       { ... σώμα συνάρτησης της κλάσης line ...}
virtual void printEq() const      { ... σώμα συνάρτησης της κλάσης circle ...}

Παραθέτω αμέσως τους ορισμούς των τριών κλάσεων:

Αφηρημένη shape, υποκλάσεις line, circle

Για ευκολία τοποθέτησα και τις τρείς κλάσεις σ ένα αρχείο επικεφαλίδας με όνομα line.h το περιεχόμενο του οποίου είναι το εξής:
#ifndef  LINE
#define  LINE

/*--------------------------------------------------------------------------
	ABSTRACT class  shape	
----------------------------------------------------------------------------*/

class cmx;

// an experimental abstract class, to be used to implement 
// 1) lines defined though two points
// 2) circles defined by center and one point on circle
class shape
{
private:
	void copy(const shape & other);
protected:
	cmx  pts[2];  
public:
	shape();
	shape(const shape &  other) ;
	shape(cmx* points) ;
	shape & operator=( const shape & other );

	void setPts(cmx const* ps) ;
	void printPoints(bool newline=false) const;  // common to both line, circle

	virtual ~shape();
	virtual void equation(double * c) const = 0;
	virtual void PtsFromc( double * c ) = 0;
	virtual void printEq() const =0;
	virtual double  eval( const cmx & z ) const = 0;
};


/*--------------------------------------------------------------------------
	class line deriving from abstract class shape
----------------------------------------------------------------------------*/
class line : public shape
{
public:
	line();
	line(const line &  other) ;
	line(cmx* points) ;
	line & operator=( const line & other );

	virtual ~line() ;
	virtual void equation(double * c) const ;
	virtual void PtsFromc( double * c ) ;
	virtual void printEq() const;
	virtual double  eval( const cmx & z ) const ;
};

/*--------------------------------------------------------------------------
	class circle deriving from abstract class shape
----------------------------------------------------------------------------*/
class circle : public shape
{
public:
	circle();
	circle(const circle &  other) ;
	circle(cmx* points) ;
	circle & operator=( const circle & other );

	virtual ~circle() ;
	virtual void equation(double * c) const ;
	virtual void PtsFromc( double * c ) ;
	virtual void printEq() const;
	virtual double  eval( const cmx & z ) const ;
};

#endif  // LINE

Κοινά στοιχεία των τριών κλάσεων

Το πρώτο που θέλω να παρατηρήσω είναι ότι όλες οι virtual εκτός του καταστροφέα εμφανίζονται με το ίδιο όνομα και ίδια μορφή και στις τρείς κλάσεις. Θα διαφοροποιηθούν αργότερα όταν θα γράψω τα σώματά τους. Το μηδενικό σώμα σε μιά συνάρτηση της αφηρημένης κλάσης δηλώνεται με το = 0 που ακολουθεί την εκφώνησή της.
Συναρτήσεις που κάνουν την ίδια δουλειά και στις δύο υποκλάσεις, τοποθετούνται στην αφηρημένη ώστε να είναι διαθέσιμες και στις δύο. Αυτό συμβαίνει λ.χ. με τις συναρτήσεις:
  void setPts(cmx const* ps) ;
  void printPoints(bool newline=false) const;  // common to both line, circle
Η πρώτη δίνει τιμές στα δύο σημεία που ορίζουν τον κύκλο ή την ευθεία. Η δουλειά είναι η ίδια και στις δύο περιπτώσεις, άρα τοποθετείται εδώ (στην αφηρημένη μητέρα κλάση).
Η δεύτερη τυπώνει τα δύο αυτά σημεία, άρα και αυτή κάνει την ίδια δουλειά και στις δύο υποκλάσεις.
Την shape την σκέφθηκα να έχει μεταβλητές δύο μιγαδικούς ( cmx pts[2]; ) οι οποίοι σε κάθε υποκλάση θα παίζουν διαφορετικό ρόλο:
line: Σ αυτήν τα pts[0] και pts[1] είναι δύο σημεία που καθορίζουν πλήρως την ευθεία
circle: Σ αυτήν το pts[0] είναι το κέντρο και το pts[1] είναι ένα σημείο στην περιφέρεια του κύκλου. Και σ αυτήν την περίπτωση τα δύο αυτά σημεία καθορίζουν πλήρως τον κύκλο.
Και οί δύο υποκλάσεις λοιπόν θα ορίζουν πλήρως τα αντικείμενά τους από δύο μιγαδικούς. Τους βάζω λοιπον κι αυτούς στην αφηρημένη μητέρα κλάση ως μεταβλητές της με την μορφή μιγαδικού διανύσματος δύο διαστάσεων. Η λέξη protected σημαίνει ότι οι μεταβλητές προστατεύονται και δεν είναι προσπελάσιμες παρά μόνο από την ίδια την αφηρημένη κλάση στην οποία δηλώνονται και όλες τις υποκλάσεις αυτής.

Υλοποίηση των κλάσεων

Παραθέτω εδώ τον κώδικα που ορίζει τα σώματα των συναρτήσεων των τριών προηγουμένων κλάσεων. Παρατήρησε ότι γιά τις συναρτήσεις με μηδενικό σώμα δεν γράφω καθόλου κώδικα. Τις υλοποιήσεις αυτές έχω τοποθετήσει σε αρχείο με όνομα line.cpp το περιεχόμενο του οποίου είναι:
#include "comx.h"
#include "line.h"
#include "cmxFunctions.h"
#include <iostream.h>
#include <math.h>


/*--------------------------------------------------------------------------
	ABSTRACT class  shape	
----------------------------------------------------------------------------*/


shape::shape(){}
shape::~shape(){}
shape::shape(const shape &  other) { copy(other);}
shape::shape(cmx* points) {pts[0]=points[0]; pts[1]=points[1];}

shape & shape::operator=( const shape & other )
{
	if(this == &other) return *this;
	copy(other); return *this;
}

void shape::copy(const shape & other)
{
	pts[0]=other.pts[0];
	pts[1]=other.pts[1];
}

void shape::setPts(cmx const* ps) {pts[0]=ps[0]; pts[1]=ps[1];}

void shape::printPoints(bool newline) const
{
	for(int i=0;i<2;i++)
	{
		cout << "pt(" << i << ")= " ;
		pts[i].print();
		cout << "   ";
	}
	if(newline) cout << endl;
}



/*--------------------------------------------------------------------------
	class line deriving from abstract class shape
----------------------------------------------------------------------------*/
line::line(){}
line::line(const line &  other):shape(other){}
line::line(cmx* points):shape(points){} ;
line & line::operator=( const line & other )
{
	shape::operator =(other);
	return *this;
}

line::~line() {}

void line::equation(double * c) const 
{
	c[0] = pts[0].y-pts[1].y;
	c[1] = pts[1].x-pts[0].x;
	c[2] = -(c[0]*pts[0].x + c[1]*pts[0].y);
}

void line::PtsFromc( double * c )  
{
	cmx  diff(-c[1], c[0] );
	double  lamda = (-c[2]/(c[0]*c[0]+c[1]*c[1])); // warning
	pts[0].x = lamda*c[0];
	pts[0].y = lamda*c[1];
	pts[1] = pts[0] + diff;
}

void line::printEq() const
{
	double c[3];
	equation(c);
	cout << c[0] << "X + " << c[1] << "Y + " << c[2] << " = 0 \n";
}

double  line::eval( const cmx & z ) const 
{
	double c[3];
	equation(c);
	return c[0]*z.x + c[1]*z.y+ c[2];
}


/*--------------------------------------------------------------------------
	class circle deriving from abstract class shape
----------------------------------------------------------------------------*/
circle::circle(){}
circle::circle(const circle &  other):shape(other){}
circle::circle(cmx* points):shape(points){} ;
circle & circle::operator=( const circle & other )
{
	shape::operator =(other);
	return *this;
}

circle::~circle() {}

void circle::equation(double * c) const 
{
	c[0] = -2.*pts[0].x;
	c[1] = -2.*pts[0].y;
	c[2] = 2.*inner(pts[0],pts[1])-inner(pts[1],pts[1]);
}

void circle::PtsFromc( double * c )   
{
	pts[0].x = - 0.5* c[0];
	pts[0].y = - 0.5* c[1];
	double  critical = inner(pts[0],pts[0])-c[2];
	if( critical < 0.0 )
	{
		cout << " ERROR no real circle " << endl;
		pts[1]= pts[0];					// warning  ???????
	}
	else
	{
		double radius = sqrt( critical );
		pts[1].x  = pts[0].x + radius;
		pts[1].y  = pts[0].y;
	}
}

void circle::printEq() const
{
	double c[3];
	equation(c);
	cout  << "X2+Y2+ " << c[0] << "X + " << c[1] << "Y + " << c[2] << " = 0 \n";
}

double  circle::eval( const cmx & z ) const 
{
	double squares = inner(z,z);
	double c[3];
	equation(c);
	return squares + c[0]*z.x + c[1]*z.y+ c[2];
}

operator=

Ο τελεστής ισότητας είναι, κατά κανόνα, μιά απαραίτητη συνάρτηση σε κάθε κλάση. Καθορίζει τι ακριβώς εννοούμε όταν γράφουμε α=β. Στην περίπτωσή μας εδώ σημαίνει ισότητα των αντιστοίχων μεταβλητών της α και β, πράγμα που αποκαθίσταται με την συνάρτηση copy. Το ερώτημα if αντιμετωπίζει την περίπτωση μιάς ισότητας της μορφής α=α. Σημειώνω ότι η εντολή a = b; είναι ισοδύναμη με την a.operator=(b); Χρησιμοποιούμε την ισότητα σαν μιά κοινή συνάρτηση που παίρνει σαν όρισμα ένα αντικείμενο (αναφορά για την ακρίβεια) της κλάσης και επιστρέφει ένα αντικείμενο της ίδιας πάλι κλάσης, ώστε να μπορούμε να γράψουμε a = b = c = ... κτλ.
Κάθε κλάση θέλει την δική της ισότητα και δεν μπορεί να χρησιμοποιηθεί η ισότητα της μητέρας κλάσης. Κάθε υποκλάση της αφηρημένης κλάσης πρέπει να υλοποιήσει την δική της ισότητα.

Επιλεκτική ανάλυση του κώδικα

Αναλύω επιλεκτικά τον κώδικα που παρουσιάζει κάποιο νέο στοιχείο ή ιδιαίτερο ενδιαφέρον.
line::line(const line & other):shape(other){} Ο κατασκευαστής-αντιγραφής, καλεί απλά τον αντίστοιχο της μητέρας κλάσης. Το ίδιο συμβαίνει και με τον αντίστοιχο κατασκευαστή της circle.
line & line::operator=( const line & other ) { shape::operator =(other); return *this; } Ο τελεστής ισότητας της line καλεί τον αντίστοιχο της μητέρας κλάσης και επιστρέφει το ίδιο το αντικείμενο: (*this) που είναι το αριστερό μέλος της ισότητας.
void line::equation(double * c) const Συνάρτηση που υπολογίζει τα c[0], c[1], c[2] που παριστούν τους συντελεστές της εξίσωσης της ευθείας: c[0]x + c[1]y + c[2] = 0.
Η ομόνυμη συνάρτηση της circle υπολογίζει τους αντίστοιχους συντελεστές της εξίσωσης του κύκλου: x^2+y^2 + c[0]x + c[1]y + c[2] = 0.
Το πρόβλημα είναι ότι σε κάθε κλάση χρειάζεται ένας διαφορετικός τρόπος υπολογισμού αυτών των συντελεστών από τους δύο μιγαδικούς pts[0] και pts[1]. Ετσι κάθε κλάση έχει την δική της συνάρτηση γι αυτόν τον υπολογισμό.
void line::PtsFromc( double * c ) Συνάρτηση που κάνει την αντίστροφη δουλειά από την προηγούμενη. Δηλαδή υπολογίζει τα σημεία pts[0] και pts[1] από τα c[0], c[1], c[2] που παριστούν τους συντελεστές της εξίσωσης της ευθείας: c[0]x + c[1]y + c[2] = 0.
Η ομόνυμη συνάρτηση της circle κάνει τα ανάλογα για τον κύκλο.
void line::printEq() const Συνάρτηση που τυπώνει την εξίσωση της ευθείας
Η ομόνυμη συνάρτηση της circle κάνει τα ανάλογα για τον κύκλο.

Πρόγραμμα που χρησιμοποιεί τις τρεις κλάσεις

Σ΄ένα αρχείο με όνομα cmx.cpp γράφω τον κώδικα:
#include "comx.h"			 
#include "line.h"       
#include <iostream.h>

int main(int argc, char* argv[])
{
	cmx   a[2] = { cmx(1.,1.), cmx( 1.0, 0.0) };
	cmx   b[2] = { cmx(-1.,0.), cmx( 0.0, 1.0) };
	line  aLine(a);
	line  bLine(b);
	aLine.printEq();
	bLine.printEq();  // sign printing problem ??

	double  c[3] = { -1. , 1. , -1. };
	line cLine;
	cLine.PtsFromc(c); 
	cLine.printEq();

	circle aCircle(a);
	circle bCircle(b);
	aCircle.printEq();
	bCircle.printEq();
	
	circle cCircle;
	cCircle.PtsFromc(c);
	cCircle.printEq();
	return 0;
}

Εκτέλεση του προγράμματος

Ασκήσεις

1)   Βρές στον κώδικα παραπάνω τις θέσεις που έχω γράψει το σχόλιο warning δηλαδή προσοχή. Γιατί το κάνω αυτό; Τί ενδεχόμενο ή απρόοπτο μπορεί να προκύψει;
2)   Τι δουλειά κάνει η συνάρτηση eval;
3)   Είναι η επόμενη ισοδύναμη υλοποίηση του τελεστή ισότητας ή όχι;
shape & shape::operator=( const shape & other )
{
	if(this != &other) copy(other);
	return *this;
}
Εξήγησε τι είναι το this και τι κάνει η συνάρτηση αυτή.