PROGRAMMAZIONE AD OGGETTI 2001-2002
16-1-2002
Una caratteristica molto importante delle classi è la possibilità di realizzare delle associazioni tra le classi di tipo ereditario. Da una classe "madre" possono essere definite delle nuove classi "figlie" che conservano tutte le caratteristiche della classe "madre" e ne aggiungono delle nuove o ne modificano alcune. Tutte le classi ereditate si assomigliano e servono a definire degli aspetti più specifici della classe madre. I rapporti di ereditarietà tra le classi sono esprimibili con dei grafi; un triangolo è il simbolo con cui si indica ereditarietà.
Un esempio di ereditarietà può essere questo: da una classe AUTO, con i propri metodi e attributi, si possono derivare delle classi più specifiche come FIAT, MERCEDES, FERRARI... che conservano gli attributi ed i metodi della classe AUTO e magari contengono dei nuovi metodi, che però sono diversi per ogni classe ereditata. Inoltre si può continuare la gerarchia delle classi facendo derivare per esempio dalla classe FIAT le classi UNO, PUNTO..., dalla classe MERCEDES le classi SLK, CLK... , dalla classe FERRARI le classi TESTAROSSA, GTO...; queste classi derivate saranno sempre più specifiche e sempre diverse le une dalle altre, visto che derivano da classi diverse. Questo aspetto delle classi è molto sfruttato perchè permette di prendere delle classi già fatte e di modificarle aggiungendo solo delle piccole parti di codice.
N.B.: le classi ereditate sono molto diverse dalle classi annidate. Un esempio di classi annidate è il seguente: car.cpp
L'ereditarietà permette di aggiornare una classe facendo derivare da questa una classe derivata alla quale si possono portare le opportune modifiche senza dover ripartire da zero. La classe derivata conserva tutte le associazioni della classe base e per questo può essere utilizzata al suo posto.
Si può ottenere una nuova classe che fa una certa funzione facendola derivare da una classe base che svolge una funzione simile e modificandone alcune caratteristiche. Questo può essere fatto anche senza sapere come funziona la classe base (cioè senza dover disporre del codice ma solo con il programma eseguibile e l'header file che contiene la classe base), ma semplicemente aggiungendo del codice opportuno che risolve i problemi. Non è sempre facile però aggiungere le parti mancanti o differenti in modo corretto, e spesso si commettono errori. Questa viene chiamata anche programmazione per differenza: nuove parti di programma possono essere costruite a partire da parti già realizzate aggiungendo quello che manca (cioè la differenza).
TRASMISSIONE DI METODI E ATTRIBUTI
Le classi derivate conservano tutti gli attributi ed i metodi della classe base che non devono essere nemmeno dichiarati all'interno della nuova classe ma sono direttamente utilizzabili. I metodi e gli attributi che vengono dichiarati all'interno della classe derivata sono invece propri solo della classe derivata. I metodi e gli attributi della classe derivata però non sono assolutamente in comune con quelli della classe base. Gli attributi "private" della classe base sono comunque accessibili solo dalle classi derivate e non dall'esterno. E' possibile fare l'overloading dei metodi della classe base, con una sintassi che si può vedere in seguito.
In una classe possono essere dichiarati dei metodi astratti, cioè dei metodi vuoti, che non fanno nulla o che addirittura sono solo dichiarati. La classe che contiene questi metodi viene chiamata classe astratta. Le classi di questo tipo nascono proprio come "modelli" per ottenere delle classi derivate più specifiche, più dettagliate. In questo modo si può definire una classe "a grandi linee" specificando ad esempio quali sono i metodi che potrebbe utilizzare, ma questi metodi non sono definiti, solo dichiarati. Per usare la classe astratta bisogna fare una classe derivata e definire il corpo dei metodi astratti. Il compito del programmatore è quello di sfruttare la struttura della classe astratta e di definire i metodi astratti nel modo opportuno. Non si possono istanziare oggetti da una classe astratta perchè il compilatore trova dei metodi dichiarati (i metodi astratti) ma che non sono definiti da nessuna parte. Le classi derivate da una classe astratta possono istanziare oggetti solo se i metodi astratti sono stati definiti (sono stati resi completi). Ecco un esempio di classe astratta:
class A
{int x;
public:
virtual void stampa()=0;
/* metodo astratto puro (=0) */};
Nella sintassi del C++ un metodo astratto va preceduto dalla parola chiave "virtual" che sta ad indicare che il metodo è solo dichiarato e verrà definito in seguito. Una classe derivata deve definire il corpo del metodo stampa() per poter istanziare oggetti.
Se una classe astratta contiene solamente metodi astratti allora questa è un' interfaccia.
In C++ c'è una sintassi ben definita per specificare la relazione di ereditarietà tra le classi. Per definire una classe derivata dalla classe A si agisce nel seguente modo:
class B: public A
{/* la classe B è derivata della classe A */
int y;
/* y è un nuovo attributo, che non è accessibile da A */public:
B(int yy) :y(yy), x(xx) {}
/* costruttore */virtual void stampa();
/* bisogna specificare che il metodo è virtual per poterlo definire */void azzera();
};
...
B::stampa()
{cout << "il valore di y è: " << y << endl;
cout << "il valore di x è: " << x << endl;
/* può accedere liberamente a x che sebbene sia dichiarata in A è stata ereditata anche da B */}
...
void main()
{B b(3,4);
b.stampa();
/* stampa sia il valore di y che di x */}
La classe B quindi può utilizzare liberamente gli attributi che ha ereditato dalla classe A come se questi attributi fossero dichiarati direttamente all'interno della classe B. Il metodo stampa deve essere obbligatoriamente definito poichè è definito come metodo virtuale puro: al posto delle parentesi graffe c'è un "=0".
Attenzione: solo i metodi che sono dichiarati virtual possono essere definiti nelle classi derivate. I metodi che non sono definiti virtual non possono essere ridefiniti nelle classi derivate.
C'è differenza tra una metodo dichiarato virtual e un metodo virtual puro: un metodo virtual puro non ha corpo e la sua dichiarazione finisce con "=0"; un metodo virtual invece può avere corpo e può anche essere definito. Allora un metodo virtual puro deve essere necessariamente definito prima di essere utilizzato; un metodo virtual invece può essere utilizzato così come è definito nella classe base, oppure può essere ridefinito sovrapponendosi a quello base (è una sorta di metodo di default che può essere ridefinitonella funzione derivata). Solitamente all'interno di un metodo virtual c'è la stampa di una stringa che ricorda appunto che il metodo è "finto" ed è meglio ridefinirlo prima di usarlo:
virtual cerca ()
{cout << "attenzione: il metodo cerca è un metodo astratto" <<endl;
}
E' possibile però definire in una classe derivata un metodo che ha lo stesso nome di un metodo presente nella classe base e che non è virtual: questo nuovo metodo non sostituisce il metodo della classe base (come invece succede per la ridefinizione di un metodo virtual) ma ha la precedenza sul metodo della classe base.
vedi esempi: | useful.h | ||
inheritance.cpp | |||
instrument5.cpp |
Si possono definire anche dei metodi protetti, usando la parola chiave "protected:" nello stesso modo in cui si usano "private:" e "public:": questi metodi diventano protetti nella classe derivata e possono essere utilizzati solo all'interno dei metodi della classe derivata, mentre non possono essere richiamati direttamente da un oggetto.
vedi esempio: protected.cpp |