Lezione sulle classi

In questa lezione vediamo l’entità più importante che la programmazione abbia conosciuto, almeno da qualche anno a questa parte: le classi, ovvero la capacità del linguaggio di programmazione di offrire all’utente un sistema di astrazione e portare in modo più concreto la struttura dei problemi del mondo reale all’interno dell’infrastruttura informatica.

Le classi

Le classi sono in un certo un’estensione delle struttura, che abbiamo già visto. Le strutture sono raccoglitori di informazioni: ogni oggetto di una struttura possiede alcuni altri oggetti al suo interno (che chiamiamo proprietà), il tipo e il nome dei quali è definito dalla struttura. Per esempio, si potrebbe creare una struttura studente per rappresentare informaticamente gli studenti di questo corso: ogni studente avrà alcune proprietà, tra cui anno di nascita (unsigned), numero di matricola (int), punti ottenuti con le serie (unsigned),… Poi, chiaramente, ogni studente avrà valori diversi per le proprietà, ma tutti avranno queste proprietà dei tipi indicati.

Una classe, oltre a raccogliere informazioni, definisce delle azioni, che sono funzioni che noi chiamiamo metodi, che in un certo senso fanno parte dell’oggetto a cui si riferiscono come le proprietà fanno parte dell’oggetto cui appartengono. La classe studente, oltre a contenere le informazioni su anno di nascita, numero di matricola e punti ottenuti, potrebbe avere le azioni bool controlla_testat(), che ritorna vero se e solo se il numero di punti è maggiore o uguale a quello richiesto per ottenere il Testat; void consegna_serie(unsigned), che viene eseguita quando lo studente consegna una serie che viene valutata con il numero di punti passato come parametro: questo numero viene aggiunto al totale dello studente, facendo aumentare il numero di punti ottenuti e portandolo verso il conseguimento del Testat.

A proprietà (sotto-variabili) e metodi (sotto-funzioni) di un dato oggetto si accede tramite l’operatore punto, nel caso di oggetti, oppure tramite l’operatore freccia, nel caso di puntatori a oggetti. Vediamo appunto l’esempio di una classe Studente, che contiene come proprietà i valori matricola e punti, e come metodi controlla_testat e consegna_serie. L’esempio è visibile su Ideone. In questo momento conviene concentrarsi sulla prima parte (definizione della classe Studente) e l’ultima (funzione main), lasciando per il momento da parte il settore centrale, con la definizione dei metodi.

Visibilità di metodi e proprietà

Metodi e proprietà possono essere pubblici o privati. Metodi e proprietà pubblici di un oggetto sono raggiungibili da qualunque altro oggetto, mentre metodi e proprietà privati possono essere usati soltanto dall’oggetto stesso o un altro oggetto della stessa classe. Prendiamo un esempio semplice: la classe C ha alcune proprietà e alcuni metodi.

class C {
public: // <-- public: introduce una sezione di metodi e proprieta' pubblici
  void metodo_pubblico();
  int proprieta_pubblca;
private: // <-- private: introduce una sezione di metodi e proprieta' privati
  void metodo_privato();
  double proprieta_privata;
};

I metodi

I metodi sono funzioni che vengono applicate su un oggetto particolare. Tale oggetto è quello che precede il punto nell’espressione in cui si richiama il metodo. Per esempio, se la classe Studente possiede il metodo controlla_testat, e Andrea è un oggetto di questa classe, allora l’espressione Andrea.controlla_testat() richiama la funzione controlla_testat — come se fosse una funzione comune, quindi con un’eventuale lista di parametri, un valore di ritorno e un’eventuale modifica alla memoria — sull’oggetto Andrea. Richiamare un metodo su di un oggetto significa passargli un parametro implicito, che non viene elencato tra gli argomenti nella dichiarazione, né nell’espressione che lo richiama, ma che si può liberamente usare all’interno della definizione del metodo.

Prendiamo in considerazione più da vicino il metodo controlla_testat: la sua definizione (visibile nel codice messo a disposizione su Ideone nella parte centrale, poco dopo la fine della definizione della classe Studente) è la seguente:

bool Studente::controlla_testat() const
{
// Viene richiamata la variabile punti, che fa parte dello studente
// (dell'oggetto) su cui il metodo e' richiamato.
return (punti >= 48);
}

Nel codice si fa riferimento alla variabile punti. Dato che all’interno dello stesso blocco (o di un eventuale blocco superiore, che però qui non c’è) non esiste la dichiarazione di questa variabile, il compiler va a cercarla quale proprietà del’oggetto Studente: la variabile punti in questo caso sarà Andrea.punti, ossia la proprietà punti dell’oggetto su cui il metodo è richiamato. È stato nominato poche righe sopra un parametro implicito; questo parametro è un puntatore di tipo Studente che indica l’oggetto su cui il metodo è richiamato. Il nome di questo puntatore è this — this è una parola riservata del C++, per cui non si può dichiarare un’altra variabile o una funzione con questo nome — ed è disponibile in ogni metodo di qualunque classe. Il codice scritto qui sopra per definire il metodo controlla_testat è in tutto e per tutto equivalente a quello che segue:

bool Studente::controlla_testat() const
{
// Viene richiamata la variabile punti, che fa parte dello studente
// (dell'oggetto) su cui il metodo e' richiamato.
return (this->punti >= 48);
}

In quest’ultimo caso si usa il puntatore this, lo si dereferenzia e si richiede la proprietà punti dell’oggetto in questione.

Metodi costanti

Un metodo può promettere di non cambiare l’oggetto su cui viene applicato. Nello stesso esempio presentato, il metodo controlla_testat non cambia alcunché dello studente che controlla; invece, il metodo consegna_serie modifica la proprietà punti dello studente. Per indicare che il primo metodo non modifica l’oggetto in questione si può (si dovrebbe, ove possibile) posporre alla lista di parametri la parola const, come è stato fatto per il metodo controlla_testat. Occorre segnalare questo fatto sia nella dichiarazione, sia nella definizione del metodo (cioè sia all’interno della definizione della classe, dove si dichiarano i vari metodi, sia dopo, quando si implementano i metodi). Nei fatti, in un metodo non costante della classe Studente, il parametro implicito this è di tipo Studente* const, mentre in un metodo costante this è di tipo const Studente* const. In fondo, quel const dopo la lista dei parametri modifica il parametro implicito.

Un metodo costante non può modificare alcuna proprietà, né richiamare alcun metodo non costante. Per esempio, il metodo controlla_testat, che è costante, non può richiamare al suo interno il metodo consegna_serie, che non lo è. Invece il contrario è perfettamente legale.

Costruttori e distruttori

Un costruttore è un metodo particolare che ha lo stesso nome della classe e che non ha alcun tipo di ritorno. Per ogni oggetto creato, viene richiamato su di lui un costruttore al momento dell’inizializzazione. Prendiamo come esempio la classe Numero (vedi il sorgente su ideone). Essa possiede una proprietà privata valore e due metodi per accedere ad essa: un getter (get_valore) e un setter (set_valore). Oltre a ciò, ha due costruttori e un distruttore (di cui ci occuperemo dopo). Ogni classe può avere un numero qualsiasi di costruttori, purché, come avviene per tutte le funzioni o i metodi che hanno lo stesso nome, questi differiscano per almeno un parametro. Nel nostro caso, il primo costruttore non ammette alcun parametro, mentre il secondo ne accetta — e ne esige — uno. Scopo dei costruttori è quello di preparare l’oggetto per il suo utilizzo e, soprattutto, inizializzare le proprietà. Come si può evincere da questi esempi molto semplici, entrambi i costruttori inizializzano la proprietà valore, ma, mentre il primo assegna a valore sempre 0, il secondo lacsia scegliere all’utente quale numero debba essere immagazzinato nella proprietà. Nella funzione main vengono esati entrambi questi costruttori.

Un costruttore comprende come ogni altro metodo un corpo di funzione in cui ogni tipo di istruzione può essere eseguito. Ma, peculiarità dei costruttori, essi comprendono anche una lista di inizializzazione, che viene introdotta con un due punti dopo la lista dei parametri e prima del corpo, e che contiene una serie di inizializzazioni delle proprietà dell’oggetto. Nel nostro caso viene inizializzata l’unica proprietà, valore. Ogni proprietà inizializzata viene separata con una virgola. Come abbiamo visto durante il corso, non tutte le variabili hanno bisogno di un’inizializzazione: nel nostro caso, si potrebbe benissimo lasciare momentaneamente valore inizializzata e darle in seguito un valore, all’interno del corpo dei costruttori. Ma ci sono variabili devono essere necessariamente inizializzate, come le variabili costanti o le referenze. In questi casi la lista di inizializzazione è l’unico posto in cui queste variabili possono essere inizializzate. Un esempio su ideone mostra questo tipo di inizializzazione.

I distruttori, invece, sono metodi che vengono richiamati nel momento in cui l’oggetto viene cancellato, cioè alla fine della funzione in cui è stato creato oppure alla fine dell’istruzione nel caso di oggetti temporanei (si veda l’esempio della classe Numero). Il nome di un distruttore è lo stesso nome della classe preceduto da una tilde (quindi il ditruttore della classe Numero avrà il nome ~Numero). Può esistere un solo distruttore in ogni classe e questo non può prendere alcun parametro. Il distruttore è particolarmente utile nei casi in cui la classe immagazzina delle variabili istanziate nello heap tramite l’operatore new. Per esempio, una classe che deve contenere un array dinamico di dati è anche responsabile di liberare la memoria con un delete (o meglio, con un delete[], trattandosi di un array): questo può essere usato in un distruttore, in modo che lo spazio venga liberato quando non ce n’è più bisogno.