Funzioni Virtuali

Definizione

Date prima un'occhiata a questo esempio:
class Father
{
public:
  void MakeAThing();
};
class Child : public Father
{
public:
  void MakeAThing();
};
...
{
  Father *obj;
  obj = new Child();
  obj->MakeAThing();    // Quale metodo verrà chiamato ?
}
Una spiegazione semplice (non stiamo ancora parlando di funzioni virtuali però questo esempio serve ad introdurre il discorso): obj è di tipo Father *. La classe Child è figlia della classe Father (come consuetudine... Ndt: in inglese Child significa proprio "figlio/figlia" e Father significa "padre"). Pertanto è possibile scrivere un valore di tipo Child * (quello ritornato dall'operatore new) nella variabile obj (sì come è possibile scrivere un valore di tipo Child in una variabile di tipo Father).

Allora che succede? Quasi sicuramente penserai: siccome obj è dichiarato dalla classe Child, sarà il metodo MakeAThing della classe Child ad essere chiamato. Ma non è così. Rifletti: il compilatore sa solo che obj è di tipo Father *, perciò chiamerà il metodo di Father. Infatti non c'è niente che dica al compilatore che la funzione membro da chiamare dipende dalla classe.

Ebbene, esiste un modo per indicare questo al compilatore. Se dichiari le funzioni "overloaded" come virtuali, tramite la keyword virtual, indichi al compilatore, che sa che questa funzione cambia da una classe ai suoi figli, di scegliere dinamicamente la funzione da chiamare, in modo che corrisponda al tipo dell'oggetto. Confrontate questo esempio con quello precedente:

class Father
{
public:
  virtual void MakeAThing();
};
class Child : public Father
{
public:
  virtual void MakeAThing();
};
...
{
  Father *obj;
  obj = new Child();
  obj->MakeAThing();    // ora viene chiamato il metodo di Child !
}

Classi Astratte

In C++ si possono definire delle funzioni virtuali pure. Una classe che contenga questo tipo di funzioni è essa stessa solo virtuale e non può essere instanziata. Ma ciascuna delle sue classi figlie dovrà dichiarare quella funzione (vedi overloading di funzioni membro), e inoltre dovrà poi ridefinirla.

Per dichiare una funzione come solo virtuale, aggiungi "= 0" alla fine della sua dichiarazione.

#include <stdio.h>

class Father
{
public:
  virtual void Display() = 0;
};

class Child : public Father
{
  void Display() { printf ("Sono una figlia della Classe Father.\n"); }
};

int main()
{
  Father *f = new Father();  // NON PERMESSO (la classe Father è solo-virtuale)
  Child *c = new Child();    // OK. Child è una normale classe (non virtuale)
  Father *f = new Child();   // Puoi dichiarare dei puntatore a Father

  return 0;
}
Notare che si può definire un puntatore ad una classe astratta, ma non si può instanziarla.

Le classi astratte sono usate per dichiarare componenti comuni per le classi che ne derivano. Quando erediti da una classe astratta in una classe, dopo aver definito un suo membro dati (ehi, non definitelo private!), puoi usarlo in tutte le funzioni definite nelle classe figlie.

class Component
{
public:
  virtual void Display() = 0;
};

class Rectangle : public Component
{
public:
  virtual void Display()
  { ... }
};

class Square : public Component
{
public:
  virtual void Display()
  { ... }
};

// Dichiara una lista di puntatori a Component
Component *list[10];

...

{
  // Qual'è il bisogno di dichiarare un oggetto Component ?
  // Nessuno, sono i suoi figli che interessano
  list[0] = new Square;
  list[1] = new Rectangle;
  ...
  for (int i=0; i<10; i++)
    list[i]->Display();		// chiamata di funzione "dinamica"
}
Ndt: In questo caso la classe Component è una classe basa astratta, che serve solo a riunire le proprietà (in tal caso un solo metodo, Display() ma potrebbero esserci più metodi e campi dati) comuni all'intera gerarchia di classi che da essa deriva. Il meccanismo delle funzioni virtuali, come mostrato nell'esempio precedente, consente di avere una azione (come Display()) che ha lo stesso nome su ogni oggetto della gerarchia di classi su cui agisce, ma è implementata in modo diverso e il compilatore sceglierà automaticamente la versione giusta da chiamare. In virtù del polimorfismo è infatti possibile implementare liste eterogenee di oggetti (il vettore list ne è un semplice esempio: è una lista statica di oggetti dinamici). Nell'esempio viene chiamata prima la funzione Display() di Square, poi quella di Rectangle, ecc...


C++