• Non ci sono risultati.

Costruttori, distruttori ed ereditarietà

N/A
N/A
Protected

Academic year: 2021

Condividi "Costruttori, distruttori ed ereditarietà"

Copied!
35
0
0

Testo completo

(1)

Corso di Programmazione ad oggetti

Ereditarietà e polimorfismo a.a. 2008/2009

Claudio De Stefano

(2)

Costruttori, distruttori ed ereditarietà

■ Quando si usa l'ereditarietà bisogna tenere conto dei costruttori e distruttori. È infatti possibile che una classe base, una classe derivata o entrambe contengano una funzione costruttore e/o distruttore

■ Vi sono due problemi principali derivanti dall'uso di costruttori e distruttori quando si usa l'ereditarietà.

– In quale momento vengono chiamate le funzioni costruttore e distruttore della classe base e di quella derivata?

– Come fare per passare per passare parametri alle funzioni costruttore della classe base?

■ In generale quando si crea un oggetto di una classe derivata si chiama prima il costruttore della classe base e poi quello della classe derivata.

■ Per quanto riguarda la distruzione. Accade l'inverso: viene prima chiamato il distruttore della classe derivata e poi quello della classe base.

(3)

Costruttori, distruttori ed ereditarietà: esempio

class Base { public:

Base(){cout<<”costruzione di base”;<<endl};

~Base(){cout<<”distruzione di base”;<<endl};

};

class Derived { public:

Derived(){cout<<”costruzione di derived”<<endl;};

~Derived(){cout<<”distruzione di derived”<<endl;};

};

costruzione di base costruzione di derived distruzione di base distruzione di derived OUTPUT

#include derived.h main()

{

Derived d;

};

(4)

Costruttori, distruttori ed ereditarietà

■ Il risultato dell'esempio precedente può essere generalizzato dicendo che i costruttori sono eseguiti seguendo l'ordine di derivazione, mentre i distruttori sono eseguiti in ordine inverso rispetto a quello di derivazione.

■ Se si riflette un attimo, questa scelta è praticamente obbligata. Infatti è possibile che il costruttore della classe derivata usi alcuni dei membri inizializzati dal costruttore della classe base. E si aspetta che tali valori siano stati precedentemente iniziliazzati come previsto dal costruttore della classe base.

■ Per quanto riguarda i distruttori la motivazione è la stessa, il distruttore della classe derivata potrebbe tentare di accedere ad un'estensione della classe base che viene distrutta dal distruttore della classe base.

(5)

Passaggio di argomenti a costruttori di classe base

■ La forma generale di dichiarazione estesa di un costruttore di una classe derivata è la seguente:

costruttore_derivata(elenco-argomenti): base1(elenco- argomenti),

base2(elenco-argomenti), .

. .

baseN(elenco-argomenti) {

// Corpo del costruttore derivato .

. . }

(6)

Gerarchie di classi

Derived1

Base

Derived2

■ Derived1 e Derived2 sono specificazioni diverse di Base (ad esempio Derived1 e Derived2 aggiungono servizi diversi).

■ Diritti di accesso:

– Variabili membro: in Base sono protette, in Derived1 e Derived2 sono private;

– Funzioni membro: sono pubbliche;

– Modalità di derivazione: pubblica

(7)

Gerarchie di classi

Base Derived1 Derived2

■ Derived1 e Derived2 sono successive specificazioni di Base (per “raffinamento”).

■ Diritti di accesso:

– Variabili membro: in A e in B sono protette, in C sono private – Funzioni membro: sono pubbliche

– Modalità di derivazione: pubblica

(8)

I puntatori a tipi derivati

■ In generale, un puntatore di un dato tipo non può puntare ad un oggetto di un tipo diverso.

■ Vi è pero un'importante eccezione a questa regola che riguarda le classi derivate.

Poniamo di avere le classi B e D, con D che deriva da B. In questa situazione, un putatore di tipo B* può anche puntare ad un oggetto di tipo D.

■ Possiamo generalizzare la situazione precedente dicendo che: un puntatore ad una classe può anche essere utilizzato come puntatore ad un oggetto di una qualsiasi delle classi derivate da tale classe base.

■ La regola inversa NON vale: un puntatore ad una classe derivata non può puntare ad oggetti della classe base.

■ Inoltre, puntando ad un oggetto della classe derivata con un puntatore alla classe base è possibile accedere SOLO ai membri della classe base che sono stati importati dalla classe base.

(9)

Puntatori a tipi derivati: esempio

class Base { protected:

int i;

public:

void set_i(int a) {i=a;}

int get_i() {return i;};

};

class Derived : public Base { int j;

public:

void set_j(int a) {i=a;}

int get_j() {return i;};

};

#include derived.h main()

{

Base *bp;

Derived d;

bp = &d; // OK bp->set_i(10); // OK

cout<<endl<<bp->get_i(); // OK bp->set_j(1); // ERRORE!

cout<<endl<<bp->get_j(); // ERRORE!

Bp è dichiarato come puntatore alla classe Base, la quale non contiene le funzioni set_j e get_j.

Pertanto il compilatore segnala un errore

(10)

I puntatori a tipi derivati: accesso mediante casting

■ Per risolvere la situazione vista alla slide precedente si può usare una conversione di tipo mediante casting:

#include derived.h main()

{

Base *bp;

Derived d;

bp = &d;

. . .

((derived *)bp)->set_j(1); // OK

cout<<endl<<((derived *)bp)->get_j(); // OK };

(11)

I puntatori a tipi derivati: aritmetica dei puntatori

■ È importante ricordare che l'aritmetica dei puntatori fa riferimento al puntatore base. Per questo motivo, quando un puntatore base punta ad un oggetto derivato, l'incremento del puntatore non fa in modo che questo punti all'oggetto successivo, del tipo derivato. Al contrario esso punterà a quello che dovrebbe essere l'oggetto successivo del tipo base.

■ Il seguente programma viene compilato, ma è semanticamente sbagliato

#include derived.h main()

{

Base *bp;

Derived d[2];

bp = d;

d[0].set_i(1);

d[1].set_i(2); .

cout<<endl<<bp->get_i();

bp++;

cout<<endl<<bp->get_i();

Equivale a bp = bp+ sizeof(Base) Visualizza un valore casuale

(12)

Il Polimorfismo

(13)

Il polimorfismo

POLIMORFISMO

“un interfaccia, più metodi”

(14)

Il polimorfismo

■ Il meccanismo del polimorfismo consente ad un'interfaccia di controllare l'accesso ad una classe generale di azioni. La specifica azione sarà selezionata in base alla particolare situazione in atto.

■ L'utilizzazione di un'unica interfaccia aiuta a ridurre la complessità dei programmi.

■ Sarà compito del compilatore automatizzare la selezione dell'azione specifica (ovvero il metodo), da applicare ad una specifica situazione.

■ Il polimorfismo è supportato dal C++ sia al momento della compilazione (compile- time) che a tempo di esecuzione (run-time).

(15)

Il polimorfismo: esempio

■ Un programma potrebbe definire tre tipi diversi di stack: uno per gli interi, uno per i caratteri e uno per i float.

■ Grazie al polimorfismo sarà possibile usare un solo insieme di nomi (push, pop, ecc.) per accedere alle funzioni specifiche dei tre tipi di stack definiti.

■ Ovviamente nel programma verranno implementate tre diverse funzioni per ogni specifica azione, ma il nome resterà lo stesso.

■ Sarà poi il compilatore a selezionare automaticamente il tipo di funzione corretta sulla base del tipo di stack utilizzato.

(16)

Le funzioni virtuali

■ Il polimorfismo viene realizzato in C++ per mezzo delle funzioni virtuali.

■ Una funzione virtuale è una funzione membro dichiarata come virtual in una classe base e ridefinita in una classe derivata.

■ Quando si eredita una classe contenente una funzione virtuale, la classe derivata ridefinisce la funzione virtuale secondo le proprie esigenze.

■ Il compito principale della funzione virtuale definita nella classe base è quello di definire appunto al forma dell'interfaccia della funzione.

■ Le ridefinizioni nelle classe derivate, invece, implementa le specifiche azioni relative alle situazioni gestite dalla classe derivata stessa.

(17)

Le funzioni virtuali

■ Quando si accede alle funzioni virtuali, per mezzo dell'operatore . (punto), tali funzioni si comportano come qualsiasi altra funzione membro.

■ La potenzialità delle funzioni virtuali, viene sfruttata quando si accede loro tramite puntatore(mediante l'operatore ->). Questo tipo di accesso consente di realizzare il polimorfismo run-time.

■ In precedenza abbiamo visto che i puntatori alla classe base possono puntare a qualsiasi classe derivata dalla base.

■ Quando un puntatore base punta ad un oggetto derivato che contiene una funzione virtuale, il C++ determina quale funzione chiamare sulla base del tipo di oggetto puntato. Questa determinazione viene eseguita run-time.

■ Pertanto, al variare del tipo di oggetto derivato puntato, cambia anche la versione della funzione virtuale che verrà eseguita.

(18)

Le funzioni virtuali: esempio

class Base { public:

virtual void show() { cout<<endl<<”oggetto della classe Base”;};

};

class Derived1 : public Base{

public:

void show() { cout<<endl<<”oggetto della classe Derived1”;};

};

class Derived2 : public Base{

public:

void show() { cout<<endl<<”oggetto della classe Derived2”;};

};

(19)

Esempio di polimorfismo run-time

#include derived.h

main() {

Base *bp, b;

Derived1 d1;

Derived1 d2;

int choice;

cout<<endl<<”a quale oggetto vuoi visualizzare?”

cout<<endl<<”0: Base”;

cout<<endl<<”1: Derived1”;

cout<<endl<<”2: Derived2”;

cin>>choice;

switch (choice) { case (0): bp = &b;

break;

case (1): bp = &d1;

break;

case (2): bp = &d1;

break;

}

bp->show();

(20)

Esempio di polimorfismo compile-time

#include derived.h main()

{

Base b;

Derived1 d1;

Derived1 d2;

int choice;

cout<<endl<<”<quale oggetto vuoi visualizzare”

cout<<endl<<”0: Base”;

cout<<endl<<”1: Derived1”;

cout<<endl<<”2: Derived2”;

cin>>choice;

switch (choice) {

case (0): b.show();

break;

case (1): d1.show();

break;

case (2): d2.show();

break;

} };

(21)

Differenze tra overloading di funzioni e funzioni virtuali

■ A prima vista, il meccanismo delle funzioni virtuali sembra simile a quello dell'overloading, ma di fatto si tratta di meccansimi totalmente diversi.

■ La principale differenza sta nel fatto che il prototipo di una funzione virtuale deve corrispondere esattamente a quello della classe base.

■ Nel caso dell'overloading delle funzioni, invece, il numero e il tipo di parametri deve modificare. Infatti è proprio questa differenza nel numero e nel tipo di parametri a guidare il compilatore nella determinazione della funzione giusta.

■ Un'altra importante differenza sta nel fatto che le funzioni costruttore, non possono essere virtuali.

(22)

Ereditarietà dell'attributo virtual

■ Quando viene ereditata una funzione virtuale viene ereditata anche la sua natura virtuale.

■ Questo significa che quando una classe derivata che abbia ereditato una funzione virtuale viene a sua volta utilizzata come classe base per un ulteriore classe derivatal funzione resta virtuale.

class Base { public:

virtual void show() { cout<<endl<<”oggetto della classe Base”;};

};

class Derived1 : public Base{

public:

void show() { cout<<endl<<”oggetto della classe Derived1”;};

};

class Derived2 : public Derived1{

public:

void show() { cout<<endl<<”oggetto della classe Derived2”;};

(23)

Le funzioni virtuali gerarchiche

■ È importante osservare che non è obbligatorio ridefinire una funzione virtuale.

■ Infatti, se una classe derivata non ridefinisce una funzione virtuale, quando un oggetto della classe derivata tenterà di accedere a tale funzione, verrà utilizzata la funzione definita nella classe base.

■ La regola generale è che quando una classe derivata non ridefinisce una funzione virtuale, verrà utilizzata la prima ridefinizione presente in ordine inverso di derivazione.

(24)

Le funzioni virtuali gerarchiche: esempio 1

class Base { public:

virtual void show() { cout<<endl<<”oggetto della classe Base”;};

};

class Derived1 : public Base{

public:

void show() { cout<<endl<<”oggetto della classe Derived1”;};

};

class Derived2 : public Base{

public:

// Non viene sostituita show() della base };

Base *bp;

Derived2 d2;

bp = &d2;

(25)

Le funzioni virtuali gerarchiche: esempio 2

class Base { public:

virtual void show() { cout<<endl<<”oggetto della classe Base”;};

};

class Derived1 : public Base{

public:

virtual void show() { cout<<endl<<”oggetto della classe Derived1”;};

};

class Derived2 : public Derived1{

public:

// Non viene sostituita show() della Derived2 };

Base *bp;

Derived2 d2;

bp = &d2;

Viene chiamata la show ridefinita in Derived1

(26)

Le funzioni virtuali pure

■ Negli esempi precedenti abbiamo visto che quando una funzione non viene ridefinta in una classe derivata, viene impiegata la versione definita nella classe che la precede nell'ordine di derivazione.

■ Spesso però accade che non vi è una definizione appropriata di una funzione virtuale all'interno della classe base. Ad esempio, una classe base potrebbe non essere in grado di definire l'oggetto in maniera sufficiente a consentire la definizione di una funzione nella classe base.

■ Inoltre, in alcuni casi è necessario assicurarsi che tutte le classi derivate ridefiniscano una funzione virtuale.

■ Per gestire questi due casi, il C++ prevede l'uso di funzioni virtuali pure.

(27)

Le funzioni virtuali pure

■ Una funzione virtuale pura è una funzione virtuale che non viene definita all'interno della classe base

■ Per definire una funzione viruale pura bisogna usaer la seguente sintassi:

virtual Tipo1 nome_funzione(Tipo2, p1, Tipo2 p2, ...) = 0;

■ Quando una funzione virtuale viene resa pura, ogni classe derivata DEVE fornire una propria definizione. La mancata ridefinizione della funzione virtuale pura nella classe derivata provoca una un errore di compilazione.

(28)

Le funzioni virtuali pure: esempio

#include derived.h main()

{

Dectype d;

Hextype h;

Octtype o;

cout<<endl;

d.setval(20);

d.show();

cout<<endl;

h.setval(20);

h.show();

cout<<endl;

o.setval(20);

o.show();

};

class Number { protected:

int val;

public:

void set_val(int a){val = a;};

virtual void show() = 0;

};

class Hextype : public Number{

public:

void show(){ cout<<hex<<val; } };

class Dectype : public Number{

public:

void show(){ cout<<val; } };

class Octtype : public Number{

public:

void show(){ cout<<oct<<val; }

}; 20

14 24

output

(29)

Le classi astratte

■ Una classe che contiene almeno una funzione virtuale pura è chiamata astratta.

■ Poiché una classe astratta contiene una o più funzioni per le quali non è presente alcuna definizione, non è possibile crare oggetti di quel tipo.

■ In pratica un classe astratta non può essere usata direttamente, ma può essere usata solo come base per definire classi derivate, che hanno appunto l'obbligo di implementare le funzioni virtuali pure ereditate.

■ Osserviamo che anche se non è possibile creare oggetti di classe astratte, è però possibile definire puntatori a classi astratte, al fine di consentire il polimorfismo a run-time.

(30)

Uso delle funzioni virtuali pure

■ Abbiamo visto che il polimorfismo consente di definire una classe generale di azioni e in cui l'interfaccia rimane costante e ogni derivazione definisce operazioni specifiche.

■ In pratica è possibile definire una classe base da utilizzare per definire la natura dell'interfaccia di una classe generale (astratta).

■ Ogni classe derivata implementerà le specifiche operazioni in relazione al tipo di dati impiegato da l tipo derivato.

■ Il modo più efficace per sfruttare al meglio le potenzialità del polimorfismo prevede l'uso di:

– Funzioni virtuali;

– Classi astratte;

– Polimorfismo run-time;

(31)

Uso delle funzioni virtuali

■ Utilizzando queste funzionalità è possibile passare da una gerarchia di classi che passi dal caso generale a quello specifico (dalla classe base a quelle derivate).

■ Secondo questa filosofia, si definiscono tutte le funzionalità e i metodi di interfacciamento comuni all'interno della classe base. Nei casi in cui determinate azioni possono essere implementate solo dalla classe derivatasi utilizza una funzione virtuale.

■ In pratica nella classe base si crea e si definisce tutto ciò che fa riferimento alla classe generale. Mentre i dettagli vengono implementati dalle classi derivate.

(32)

Uso delle funzioni virtuali: esempio

class convert { protected:

double ini, con;

public:

convert(double val){ ini = val;}

void set_ini(double val){ ini = val;}

double get_ini(){return ini;}

double get_con(){return ini;}

virtual void compute() = 0;

};

Classe astratta

class lit2gal : public convert{

public:

lit2gal(double val): convert(val){}

void compute(){

con = ini /3.7854;

} };

class fah2cel : public convert{

public:

fah2cel(double val): convert(val){}

void compute(){

con = (ini-32)/1.8;

} };

(33)

Uso delle funzioni virtuali: esempio

#include convert.h main()

{

convert *cp;

lit2gal l2g;

fah2cel f2c;

int choice;

cout<<endl<<”<quale conversione vuoi fare?”

cout<<endl<<”1: litri -> galloni”;

cout<<endl<<”2: Fahrenheit -> Celsius Derived2”;

cin>>choice;

switch (choice) {

case (1): cp = & l2g;

break;

case (2): cp = & f2c;

break;

}

comp_conv(cp);

};

void comp_conv(convert *c_ptr) { double tmp;

cout<<endl<<”digita valore da convertire:”;

cin>>tmp;

c_ptr->set_ini(tmp) c_ptr->compute();

cout<<endl<<”il valore convertito è: ”;

cout<<c_ptr->get_conv();

(34)

Funzioni virtuali: gestione di nuovi casi

■ L'uso del polimorfismo semplifica notevolmente l'estensione dei programmi a nuovi casi.

Esempio

class feet2met : public convert{

public:

feet2met(double val): convert(val){}

void compute(){

con = ini /3.28;

} };

(35)

Uso del polimorfismo

■ Un utilizzo importante del polimorfismo avviene nelle librerie di classi. È infatti possibile creare una libreria di classi generiche ed estendibili, utilizzabili da altri programmatori.

■ Un altro programmatore, eredita la classe generale, che definise l'interfaccia e tutti gli elementi comuni a tutte le classi da essa derivate, aggiungendo semplicemente le funzioni specifiche della classe derivata.

■ La creazione di librerie di classi astratte consente di creare e controllare l'interfaccia di una classe generale lasciando agli altri programmatori l'adattamento a situazioni specifiche.

Riferimenti

Documenti correlati

4 In tal senso la (66) esprime la regola di derivazione della somma due funzioni, immediatamente generalizzabile a n funzioni.. Graficare il rapporto cos`ı ottenuto

[r]

Mauro

Partendo dal significato geometrico del rapporto incrementale e osservando che al tendere di h a zero, il punto Q tende a P e la retta secante , passante per i punti P e Q , tende

Sia (0, +∞) −→ R una qualunque

Allora si può dire che il valore della derivata di una funzione rappresenta il coefficiente angolare della retta r s , tangente la funzione nel

[r]

La velocità istantanea indica la “rapidità” con cui varia lo spazio al variare del tempo e coincide con il coefficiente angolare della retta tangente nel punto considerato.