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...