// ex12_2_1.h class Cucina { public: Cucina () { cout << "Cucina::Cucina()\n"; } ~Cucina () { cout << "Cucina::~Cucina()\n"; } }; class Bagno { static unsigned counter; unsigned n; public: Bagno () { n = ++counter; cout << "Bagno::Bagno() " << n << '\n'; } ~Bagno () { cout << "Bagno::~Bagno() " << n << '\n'; } }; class Camera { static unsigned counter; unsigned n; public: Camera () { n = ++counter; cout << "Camera::Camera() " << n << '\n'; } ~Camera () { cout << "Camera::~Camera() " << n << '\n'; } }; unsigned Bagno::counter = 0; unsigned Camera::counter = 0; class Casa { Cucina cucina; Bagno bagni[2]; Camera camera[3]; public: Casa () { cout << "Casa::Casa()\n"; } ~Casa () { cout << "Casa::~Casa()\n"; } };
Abbiamo utilizzato le variabili statiche counter
all'interno delle classi Bagno
e Camera
per potere distinguere i diversi oggetti allocati negli array bagni
e camere
della classe Casa
. Esempio:
// ex12_2_1.cpp #include <iostream.h> #include "ex12_2_1.h" void main() { Casa casa; }
output:
Cucina::Cucina() Bagno::Bagno() 1 Bagno::Bagno() 2 Camera::Camera() 1 Camera::Camera() 2 Camera::Camera() 3 Casa::Casa() Casa::~Casa() Camera::~Camera() 3 Camera::~Camera() 2 Camera::~Camera() 1 Bagno::~Bagno() 2 Bagno::~Bagno() 1 Cucina::~Cucina()
Supponiamo ora di volere specializzare la nostra casa, supponendo ad esempio di volere una Villa
; secondo il comune senso di astrazione una villa è una casa, per cui non possiamo utilizzare la tecnica di composizione. Una definizione del genere
class Villa { Casa casa; public: Villa (); ~Villa (); }; |
è del tutto fuorviante perché una villa solitamente non contiene una casa, ma è essa stessa un tipo particolare di casa. Entra in gioco la tecnica della derivazione: una classe derivata da un'altra classe, detta base, ne eredita tutte le caratteristiche, implementazione interna e membri pubblici, nonché eventuali classi dalle quali la classe base derivi. A prima vista il concetto di derivare una classe può sembrare astratto ed esoterico, in realtà è esattamente il contrario: la possibilità di derivare classi estende il C++ verso l'astrazione umana, permettendo di stabilire legami estremamente intuitivi tra i diversi oggetti.
La derivazione può essere di tre tipi:
// ex12_2_2.h #include "ex12_2_1.h" class Giardino { public: Giardino () { cout << "Giardino::Giardino()\n"; } ~Giardino () { cout << "Giardino::~Giardino()\n"; } }; class Villa : public Casa { Giardino giardino; public: Villa () { cout << "Villa::Villa()\n"; } ~Villa () { cout << "Villa::~Villa()\n"; } };
// ex12_2_2.cpp #include <iostream.h> #include "ex12_2_2.h" void main() { Villa villa; }
output:
Cucina::Cucina() Bagno::Bagno() 1 ... Camera::Camera() 3 Casa::Casa() Giardino::Giardino() Villa::Villa() Villa::~Villa() Giardino::~Giardino() Casa::~Casa() Camera::~Camera() 3 ... Bagno::~Bagno() 1 Cucina::~Cucina()
Come si vede il costruttore della classe derivata Villa
chiama automaticamente il costruttore della classe base Casa
, prima di inizializzare i propri membri. Per motivi che saranno più chiari nel seguito, è fondamentale che ogni costruttore inizializzi la classe dalla quale essa deriva. Naturalmente anche il distruttore della classe base viene automaticamente chiamato prima della distruzione del'oggetto della classe derivata.
Nel caso la classe base non abbia un costruttore default, come invece accade in ex12_2_2.h
, bisogna utilizzare la stessa sintassi di inizializzazione dei membri in un costruttore:
// ex12_2_3.h #include <iostream.h> class Rettangolo { double a, b; public: Rettangolo (double a, double b) { cout << "Rettangolo::Rettangolo(" << a << " , " << b << ")\n"; } ~Rettangolo () { cout << "Rettangolo::~Rettangolo()\n"; } }; class Quadrato : public Rettangolo { public: Quadrato (double lato) : Rettangolo (lato, lato) { cout << "Quadrato::Quadrato(" << lato << ")\n"; } ~Quadrato () { cout << "Quadrato::~Quadrato()\n"; } };
// ex12_2_3.cpp #include "ex12_2_3.h" void main() { Rettangolo rettangolo (2, 5); Quadrato quadrato(3); }
output:
Rettangolo::Rettangolo(2 , 5) Rettangolo::Rettangolo(3 , 3) Quadrato::Quadrato(3) Quadrato::~Quadrato() Rettangolo::~Rettangolo() Rettangolo::~Rettangolo()
Abbiamo detto che in nessun caso i membri privati di una classe possono essere acceduti da una classe che da essa deriva; spesso tuttavia una classe derivata ha la necessità di accedere alla implementazione interna della sua classe base. Ci sono due possibili soluzioni per ovviare a tale problema, che esaminiamo entrambe. La prima strada consiste nell'utilizzare delle funzioni membro pubbliche (o protette) di accesso:
// ex12_2_4.h #include <iostream.h> class Rettangolo { // struttura interna della classe PRIVATA double a, b; // lati del rettangolo public: Rettangolo (double A, double B) : a(A), b(B) { } // funzioni di accesso double& lunghezza () { return a; } double getAltezza () const { return b; } void setAltezza (double B) { b = B; } // funzioni di utilita` double perimetro () const { cout << "Rettangolo::perimetro()\t"; return (a + b) * 2; } double area () const { cout << "Rettangolo::area()\t"; return a * b; } ~Rettangolo () { }; }; class Quadrato : public Rettangolo { public: Quadrato (double lato) : Rettangolo(lato, lato) { } // funzioni di accesso double getLato () const { return Rettangolo::getAltezza(); } void setLato (double lato) { Rettangolo::setAltezza(lato); Rettangolo::lunghezza() = lato; } // funzioni di utilita` double perimetro () const { cout << "Quadrato::perimetro()\t"; return Rettangolo::getAltezza() * 4; } double diagonale () const { cout << "Quadrato::diagonale()\t"; return Rettangolo::getAltezza() * 1.4142135624; } ~Quadrato () { } };
La struttura interna della classe Rettangolo
è privata, tuttavia un oggetto di tipo Quadrato
non possiede una implementazione propria ma si appoggia a quella della sua classe base; abbiamo allora fornito la classe rettangolo
di due funzioni membro pubbliche di accesso ai dati. Abbiamo volutamente utilizzato due tecniche diverse di accesso ai dati, cosa che nella pratica è fortemente sconsigliata al fine di evitare confusioni nell'utilizzatore della classe (ed anche nel programmatore di essa): la lunghezza di un Rettangolo
è acceduta tramite la funzione lunghezza()
, la quale restituisce un riferimento al dato; l'altezza viene acceduta in lettura tramite la getAltezza()
ed in scrittura dalla setAltezza()
. Il secondo dei due metodi è preferibile se la classe è direttamente utilizzabile dall'utente, in quanto consente un maggiore controllo della coerenza della struttura interna dell'oggetto; si pensi ad esempio a cosa potrebbe succedere se un utente impostasse una lunghezza negativa per un rettangolo.
La classe rettangolo
possiede inoltre due funzioni membro pubbliche che restituiscono il perimetro e l'area di esso; delle due solo della prima è stata implementata una funzione sovrapposta nella classe Quadrato
. Vediamo il seguente programma di esempio:
// ex12_2_4.cpp #include "ex12_2_4.h" void main() { Quadrato q(5); cout << "lato: " << q.getLato() << "\n"; cout << q.perimetro() << "\n"; cout << q.area() << "\n"; q.setLato (10); cout << "lato: " << q.getLato() << "\n"; cout << q.diagonale() << "\n"; }
output:
lato: 5 Quadrato::perimetro() 20 Rettangolo::area() 25 lato: 10 Quadrato::diagonale() 14.1421
Siccome un Quadrato
deriva da un Rettangolo
, esso è un Rettangolo
, ed è dunque possibile effettuare la chiamata q.area()
, la quale appartiene alla classe base; la funzione perimetro()
è stata invece reimplementata nella Quadrato
. In generale il compilatore, nel caso siano presenti funzioni sovrapposte in classi derivate, sceglie sempre la più ``vicina'' alla classe cui appartiene l'oggetto. Vediamo il seguente esempio:
// ex12_2_5.cpp #include <iostream.h> class Base { public: void stampa () { cout << "Base::stampa()\n"; } }; class Derivata1 : public Base { public: // NON c'e` la funzione stampa() }; class Derivata2 : public Derivata1 { public: void stampa () { cout << "Derivata2::stampa()\n"; } }; class Derivata3 : public Derivata2 { public: // NON c'e` la funzione stampa() }; class Derivata4 : public Derivata3 { public: void stampa () { cout << "Derivata4::stampa()\n"; } }; void main() { Derivata1 d1; d1.stampa(); // Base::stampa() Derivata2 d2; d2.stampa(); // Derivata2::stampa() Derivata3 d3; d3.stampa(); // Derivata2::stampa() Derivata4 d4; d4.stampa(); // Derivata4::stampa() }
I commenti in ogni riga del funzione main()
costituiscono l'output del programma, il quale mostra con chiarezza come il compilatore ``risalga'' alla funzione sovrapposta appartenente alla classe più specializzata. Questo espediente è molto utilizzato. Si consideri ad esempio il progetto di una libreria matematica; può essere molto utile costruire delle classi base generali e, mano a mano che il lavoro procede, derivare delle nuova classi sempre più specializzate ed ottimizzate. Nel frattempo è possibile effettuare dei test significativi utilizzando le funzioni delle classi base che, sebbene poco efficienti, consentono di avere dei risultati alla mano. In questa maniera si rispetta in pieno lo spirito produttivo del C++, che invita il programmatore a far evolvere i propri progetti, piuttosto che a creare in maniera sistematica insiemi di funzioni del tutto disconnesse tra loro, come invece poteva essere un approccio del C. Anche in questa possibilità di crescita interattiva, quasi organica potremmo definirla, è uno dei punti di forza del C++ rispetto a linguaggi di programmazione più tradizionali.