Le proprietà fondamentali introdotte dalla programmazione orientata agli oggetti sono:
l'incapsulamento,
l'ereditarietà
il polimorfismo.
L'incapsulamento consiste nel nascondere, nella sezione privata, le proprietà e i metodi che si vogliono rendere inaccessibili alle funzioni esterne alla classe. Queste, cioè, costituiscono il funzionamento interno della classe, mentre le funzioni pubbliche costituiscono i comandi messi a disposizione per utilizzare la classe. Ad esempio un'autovettura possiede un funzionamento interno realizzato con cinghie, valvole e ingranaggi, ma il guidatore aziona il volante, il cambio, le leve, i pedali, ecc. senza intervenire direttamente sulle parti meccaniche.
L'ereditarietà permette di specificare una classe base, che contiene le proprietà e i metodi comuni a tutte le classi che da questa verrano derivate. Le classi derivate ereditano le proprietà e i metodi della classe base, ma ne aggiungono altri più specifici.
Ad esempio, nel patrimonio di una biblioteca esistono libri, riviste, DVD, manuali, dizionari, ecc. Si potrebbe creare la classe base Pubblicazione, che possiede le proprietà Titolo, Prezzo, Editore, Prestito, ecc, oltre ai metodi per utilizzarle, comuni a tutte le pubblicazioni. Da questa si possono derivare le classi Libro, Rivista, ecc. e caratterizzarle con ulteriori proprietà specifiche del tipo di pubblicazione (una rivista possiede una periodicità e un numero, un DVD possiede una durata).
Una figura geometrica piana possiede due dimensioni e, se deve essere rappresentata su un dispositivo di stampa, anche una posizione, espressa tramite le coordinate (x, y) di un punto di riferimento, o ancora il colore delle linee, il colore interno. Differiscono per il calcolo dell'area e del perimetro.
Esiste quindi la classe base Figura e le classi derivate Rettangolo, Ellisse, Triangolo, ecc.
La classe Figura contiene i campi membro comuni alle classi derivate: le dimensioni della figura geometrica. Le classi derivate specificano altre proprietà e metodi che sono caratteristiche esclusive della figura geometrica.
Le classi derivate ereditano tutti i campi membro pubblici della classe base. Ad esempio se la classe base possiede il campo membro X, la classe derivata possiede lo stesso campo membro X oltre ai propri campi membro.
La sintassi della dichiarazione per derivare una classe, ad esempio Rettangolo, dalla classe Figura esige di specificare dopo il nome della classe derivata, il nome della classe base preceduto dal carattere due punti e dallo specificatore di visibilità:
class Rettangolo: public Figura { ... };
Lo specificatore di accesso public indica le limitazioni di visibilità dei campi membro, si può anche specificare protected o private. Vengono ereditati i campi membro che si trovano in quella o in una regione con visibilità meno restrittiva.
#include <iostream> using namespace std;
La classe base Figura possiede due proprietà, nella sezione protetta, ed un metodo nella sezione pubblica, che svolge le stesse funzioni del costruttore.
class Figura { protected: int Base, Altezza; public: void assegna(int a, int b) { Base=a; Altezza=b;} };
Il calcolo della superficie della figura geometrica viene affidato alla classe che descrive le proprietà specifiche della figura.
class Rettangolo: public Figura { public: int area () { return Base * Altezza; } };
class Triangolo: public Figura { public: int area () { return Base * Altezza / 2; } };
class Ellisse: public Figura { public: int area () { return (Base/2) * (Altezza / 2) * 3.14; } };
Le tre classi derivate posseggono gli stessi campi membro e gli stessi metodi della classe base ed aggiungono il metodo specifico per il calcolo della superficie.
int main () { Rettangolo ret; Triangolo trg; Ellisse els; ret.assegna (4, 5); trg.assegna (4, 5); els.assegna (4, 5); cout << ret.area() << endl; cout << trg.area() << endl; cout << els.area() << endl; return 0; }
Lo specificatore di accesso protected, in una classe, ha lo stesso effetto di private. La differenza si vede nelle classi derivate, le quali hanno accesso ai membri protetti della classe base, ma non ai membri della sezione privata. Infatti i campi membro della classe base Figura si trovano nella sezione protetta.
Si osservvi la sintassi per dichiarare una classe derivata:
class Ellisse: public Figura { }
Come conseguenza dello specificatore di accesso public, nelle dichiarazioni delle classi derivate, i campi membro ereditati dalle classi derivate hanno lo stesso permesso di accesso che hanno nella classe base.
Lo specificatore dopo i due punti indica il massimo permesso di accesso che devono possedere i campi da ereditare. Avendo specificato public la classe derivata eredita tutti i membri della classe base e li colloca in una regione che conserva la stessa visibilità.
Lo specificatore di accesso protected, invece, assegna la regione di visibilità protetta a tutti i membri pubblici ereditati dalla classe base. Lo specificatore di accetto private assegna la regione privata a tutti i membri ereditati.
Ad esempio, se la classe figlio viene derivata dalla classe genitore con la seguente dichiarazione:
class figlio: protected genitore;
tutti i campi ereditati da genitore avranno la regione di visibilità protected. In altri termini i campi pubblici di genitore diventano protetti nella classe figlio.
Una classe derivata eredita tutti i campi membro della classe base tranne:
il costruttore, il costruttore copia e il distruttore
le funzioni friend
Se occorre richiamare il costruttore della classe base bisogna indicarlo sulla riga dell'intestazione del costruttore della classe derivata:
figlio (parametri) : genitore (parametri) {}
Una classe può ereditare i membri da più classi. Nella dichiarazione della classe derivata si devono elencare, separate da virgola, tutte le classi base. Ad esempio se si definisse una classe Tavolozza per colorare, le classi Rettangolo, Ellisse e Triangolo potrebbero ereditare i suoi campi membro:
class Rettangolo: public Figura, public Tavolozza; class Ellisse: public Figura, public Tavolozza; class Triangolo: public Figura, public Tavolozza;
Un puntatore ad una classe derivata è compatibile con un puntatore alla sua classe base.
Tenendo presente questa proprietà, verrà rivisto il programma di calcolo delle aree delle figure geometriche proposto nella sezione precedente.
#include <iostream> using namespace std; class Figura { protected: int Base, Altezza; public: void assegna (int a, int b) { Base=a; Altezza=b; } }; class Rettangolo: public Figura { public: int area () { return (Base * Altezza); } }; class Triangolo: public Figura { public: int area () { return (Base * Altezza / 2); } }; class Ellisse: public Figura { public: int area () { return (Base/2) * (Altezza / 2) * 3.14; } };
int main () { Rettangolo ret; Triangolo trg; Ellisse els; Figura * fig1 = &ret; Figura * fig2 = &trg; Figura * fig3 = ⪕ fig1->assegna(4, 5); fig2->assegna(4, 5); fig3->assegna(4, 5); cout << ret.area() << endl; cout << trg.area() << endl; cout << els.area() << endl; return 0; }
Nella funzione main vengono creati tre puntatori a oggetti di classe Figura che vengono inizializzati con i riferimenti a oggetti di classi derivate. Con questi puntatori è possibile accedere solo ai campi ereditati, per richiamare il metodo area() si è dovuto specificare l'oggetto, anzichè il puntatore.
Per poter richiamare anche il metodo area() con il puntatore alla classe Figura, questa funzione membro dovrebbe essere dichiarata anche nella classe Figura, non solo nelle sue classi derivate, ma non è stato possibile perchè il calcolo dell'area è diverso per ogni figura.
Una funzione virtuale è un membro di una classe che può essere ridefinito nelle classi derivate. Una funzione membro a cui si vuole attribuire questa proprietà deve essere preceduta dalla parola virtual nella dichiarazione all'interno della classe base.
#include <iostream> using namespace std; class Figura { . . . }; class Rettangolo: public Figura {. . .}; class Triangolo: public Figura {. . .}; class Ellisse: public Figura {. . .}; int main () { .. creazione degli oggetti e dei puntatori agli oggetti .. cout << fig1->area() << endl; cout << fig2->area() << endl; cout << fig3->area() << endl; return 0; }
Tutte le classi hanno gli stessi campi membro. La funzione membro area() essendo stata dichiarata virtual nella classe base viene ridefinita in ognuna delle classi derivate.
Un metodo della classe base preceduto da virtual è presente con lo stesso nome anche nelle classi derivate e può essere richiamato tramite un puntatore a un oggetto della classe derivata.
Una classe che eredita e ridefinisce un metodo virtual è chiamata classe polimorfa.
Se in una classe base la funzione virtual viene lasciata senza definizione, semplicemente assegnando il valore zero alla funzione, la classe base diventa astratta.
virtual int area () =0;
Si è accodato =0 alla funzione virtual area () al posto della definizione della funzione. Questa è chiamata funzione virtuale pura, e tutte le classi che contengono una funzione virtuale pura sono dette classi astratte.
Non si possono creare istanze di classi astratte, però si possono creare puntatori a classi astratte e sfruttare il polimorfismo. Ecco un esempio completo:
#include <iostream> using namespace std; class Figura { protected: int Base, Altezza; public: void assegna (int a, int b) { Base=a; Altezza=b; } virtual int area (void) =0; }; class Rettangolo: public Figura { public: int area (void) { return (Base * Altezza); } }; class Triangolo: public Figura { public: int area (void) { return (Base * Altezza / 2); } }; int main () { Rettangolo ret; Triangolo trg; Figura * fig1 = &ret; Figura * fig2 = &trg; fig1->assegna(4, 5); fig2->assegna(4, 5); cout << fig1->area() << endl; cout << fig2->area() << endl; return 0; }
Si deve notare che ci si riferisce a oggetti di classi diverse usando un unico tipo di puntatore (Figura *). Adesso si crea una funzione membro nella classe base astratta che stampa il valore restituito dalla funzione area(). È importante notare che la classe base non possiede la definizione della funzione virtuale, ma la stampa sarà corretta:
#include <iostream> using namespace std; class Figura { protected: int Base, Altezza; public: void assegna (int a, int b) { Base=a; Altezza=b; } virtual int area (void) =0;
void stampaArea (void) { cout << this->area() << endl; }
}; int main () { ...
fig1->stampaArea(); fig2->stampaArea(); return 0; }
Lo stesso esempio può essere modificato per utilizzare oggetti allocati dinamicamente.
int main () { Figura * fig1 = new Rettangolo; Figura * fig2 = new Triangolo; fig1->assegna (4,5); fig2->assegna (4,5); fig1->stampaArea(); fig2->stampaArea(); delete fig1; delete fig2; return 0; }
I puntatori fig1 e fig2 sono dichiarati di tipo puntatori a Figura e viene loro assegnato il riferimento a oggetti di classi derivate.