C++: classi astratte e polimorfismo
Talvolta, può essere necessario creare un tipo di dato non inteso per essere istanziato, ma da essere utilizzato nel meccanismo di ereditarietà come classe base per fornire un’interfaccia pubblica alle altre classi. Tali classi sono dette astratte, a differenza delle classi che possiamo utilizzare per istanziare degli oggetti, dette concrete.
Le classi astratte non possono, come detto, istanziare oggetti, perchè risultano incomplete. Spetterà alle classi derivate definire le parti mancanti.
Dal punto di vista logico, una classe astratta è troppo generica per essere utilizzata direttamente. Il classico esempio è quello della classe Shape, da cui derivano, ad esempio, le classi Square e Triangle.
Disegnare uno Shape ci risulta impossibile. E’ necessario definire tale funzionalità, compito che spetta alle classi derivate.
Una classe è astratta quando dichiara una o più funzioni virtuali pure. Una funzione virtuale pura viene specificata ponendo = 0 nella sua dichiarazione:
virtual void draw() const = 0;
Le funzioni virtual pure non forniscono un’implementazione. Ogni classe concreta deve necessariamente fornire un’override dei metodi virtuali puri.
La differenza tra le funzioni virtual pure e le funzioni virtual è che, quest’ultime, forniscono un’implementazione e consentono alle classi derivate di effettuare un’overriding opzionale, mentre le funzioni virtuali pure non forniscono un’implementazione, richiesta alle classi derivate concrete (una classe che non fornisce un’implementazione di questi metodi, rimane astratta).
Il motivo per cui le funzioni virtuali pure non hanno implementazione è che non avrebbe senso fornirne una (vedi il metodo draw della classe Shape), a differenza delle funzioni virtuali (ad esempio, una classe astratta può fornire il metodo virtuale getName, che fornisce il nome dell’oggetto).
Le classi astratte possono, comunque, avere membri e funzioni concrete (come costruttori e distruttori).
Sfruttando il polimorfismo, le classi astratte hanno un’importanza vitale.
Consideriamo, in una libreria grafica, la classe astratta Widget. Tale classe fornisce i metodi virtuali puri drawOnScreen, per disegnare il widget sullo schermo, e geometry, che rappresenta la geometria dell’oggetto.
Il programma può utilizzare un puntatore ad un oggetto Widget e utilizzare i metodi indicati senza preoccuparsi di quale widget si stia utilizzando (un push button, una text box, una label…). Inoltre, l’introduzione di una nuova classe derivata da Widget (ad esempio RichTextBox, una classe che consente input con particolari formattazioni) non compromette l’uso del software, consentendo una sua facile estensione.
Vediamo in dettaglio la classe Shape, descritta precedentemente:
class Shape { public: virtual void draw() const = 0; // disegna la figura virtual void move() = 0; // muove la figura };
Si noti che provare ad instanziare tale classe comporta un’errore di compilazione:
int main() { Shape shape; }
Output del compilatore
main.cpp: In function 'int main()': main.cpp:10: error: cannot declare variable 'shape' to be of type 'Shape' main.cpp:10: error: because the following virtual functions are abstract: main.cpp:5: error: virtual void Shape::move() main.cpp:4: error: virtual void Shape::draw() const
Creiamo ora due classi concrete da Shape e vediamo che è effettivamente possibile utilizzarle:
#include <iostream> using namespace std; class Shape { public: virtual void draw() const = 0; // disegna la figura virtual void move() = 0; // muove la figura }; class Square : public Shape { public: void draw() const { cout << "Draw square..." << endl; } void move() { cout << "Move square..." << endl; } }; class Triangle : public Shape { public: void draw() const { cout << "Draw triangle..." << endl; } void move() { cout << "Move triangle..." << endl; } }; void work(Square square) { square.draw(); square.move(); } void work(Triangle triangle) { triangle.draw(); triangle.move(); } int main() { Square square; Triangle triangle; work(square); work(triangle); }
Si può notare come tutto fili liscio e di come la soluzione non sia affatto ottimale. Introdurre una classe Circle comporta l’introduzione di un nuovo overload della funzione work. Vediamo come utilizzare il polimorfismo quindi:
#include <iostream> using namespace std; class Shape { public: virtual void draw() const = 0; // disegna la figura virtual void move() = 0; // muove la figura }; class Square : public Shape { public: void draw() const { cout << "Draw square..." << endl; } void move() { cout << "Move square..." << endl; } }; class Triangle : public Shape { public: void draw() const { cout << "Draw triangle..." << endl; } void move() { cout << "Move triangle..." << endl; } }; class Circle : public Shape { public: void draw() const { cout << "Draw circle..." << endl; } void move() { cout << "Move circle..." << endl; } }; void work(Shape* shape) { shape->draw(); shape->move(); } int main() { Shape* square = new Square(); Shape* triangle = new Triangle(); Shape* circle = new Circle(); work(square); work(triangle); work(circle); delete(square); delete(triangle); delete(circle); }
L’output è, ovviamente, quello atteso:
Draw square... Move square... Draw triangle... Move triangle... Draw circle... Move circle...
circa 13 anni fa
Grazie per l’articolo veramente ben spiegato.
Ho solo un problema in fase di compilazione…
Uso Microsoft Visual C++ Express 2010 e quando vado a definire gli oggetti con l’operatore “new” và in errore e non mi fà compilare… impossibile creare un’istanza della classe astratta..vedere la dichiarazione di ‘Shape::draw’.
come mai?
Grazie
circa 13 anni fa
Ciao Romano, potresti incollarmi la linea di codice dove becchi l’errore in compilazione? Grazie.
circa 13 anni fa
#include “Counter.h”
#include
using namespace std;
class Virtual{
public:
/*
Funzione virtuali pura
In questo caso non è possibile instanziare la classe che contiene tale funzione (e tale classe prende il nome di classe astratta).
Se una classe con una funzione virtual viene ereditata, tale funzione può essere ridefinita.
*/
virtual void PrintStatus() = 0;
};
class Accumulator : public Virtual{
public:
~Accumulator(void);
Accumulator():sum(0){}
Accumulator(int n):sum(n){}
void add(int n){sum +=n;}
int getSum(){return sum;}
void printStatus(){
cout << "Sono un accumulatore e il mio contenuto e' "<< sum << endl;
}
private :
int sum;
};
class BigCounter : public Counter , public Virtual{
public:
BigCounter(void);
~BigCounter(void);
/*
l'operatore ":" invoca il costruttore di una classe madre(o più) nel prototipo
del costruttore di una classe figlia in una gerarchia ereditaria di classi.
*/
BigCounter(int n):Counter(n){}
void increment(){
Counter::increment();
Counter::increment();
}
void printStatus(){
cout << "Sono un contatore e il mio contenuto e' ";
cout<< Counter::value << endl;
}
};
int main() {
BigCounter* c1 = new BigCounter();
Accumulator* c2 = new Accumulator();
cout <getValue() << endl;
cout <getSum() <increment();
c2->add(5);
c1->printStatus();
c2->printStatus();
delete c1;
delete c2;
system(“Pause”);
return 0;
}
L’errore mi viene dato nel mio main quando vado a definire i due oggetti new BigCounter e new Accumulator.
Grazie
circa 13 anni fa
Perdonami se la risposta non è corretta ma non ho la possibilità di provare ora.
Mi salta all’occhio una cosa però:
Tu hai il metodo Virtual::PrintStatus, BigCounter::printStatus e Accumulator::printStatus. Le ‘p’ non sono maiuscole dunque non stai overridando PrintStatus di Virtual ma stai creando due nuovi metodi. PrintStatus rimane virtuale e non overridato, dunque non puoi instanziare gli oggetti.
Prova a cambiare i nomi in modo che siano gli stessi e prova a compilare. Spero tu riesca a farlo ma nel caso ciò non avvenisse, incollami il nuovo codice con i metodi che hanno lo stesso nome, così domani posso provare. Grazie.
circa 13 anni fa
G R A Z I E !!! Era proprio questo l’errore “case sensitive”…
da oggi porrò molta più attenzione!
Non ci avevo fatto caso!
Ad Majora e grazie per il lavoro che fai qui sul web!
Davvero prezioso…continuo a seguirti!
circa 13 anni fa
Grazie mille! E’ un piacere!