Costruttori e distruttori

13 febbraio 2017

La definizione di una nuova classe comporta una grande libertà per il programmatore, che può aggregare in essa molteplici tipi di dati e funzioni membro per la caratterizzazione dell’oggetto e la definizione delle sue funzionalità.

Tuttavia, per utilizzare un oggetto ci sono due operazioni preliminari da compiere:

  1. l’allocazione della memoria per tutti i suoi membri (dati e funzioni) nello heap o nello stack;
  2. l’inizializzazione dei dati membri.

Queste due operazioni sono svolte da una particolare funzione membro detta costruttore, la cui firma presenta delle caratteristiche differenti rispetto a quelle di una ordinaria funzione membro:

  • il nome del costruttore coincide con quello della classe;
  • un costruttore non ha un valore di ritorno.

Tuttavia, un costruttore può accettare uno o più argomenti, anche opzionali, e può essere sovraccaricato.

Un costruttore che non accetta argomenti, o in cui tutti gli argomenti sono opzionali, è detto costruttore di default e se la definizione della classe non include nessun costruttore, il compilatore, salvo alcuni casi particolari, lo genera automaticamente per noi. La quantità di spazio da riservare è infatti un’informazione che può essere dedotta facilmente a tempo di compilazione, ed i valori predefiniti per ogni dato membro saranno determinati in base al loro tipo.

Tuttavia, il costruttore di default generato dal compilatore ha dei limiti. Ad esempio, il valore predefinito per il tipo di ogni dato membro può dipendere dal compilatore utilizzato. Ciò vale anche quando uno o più dati membri della classe sono puntatori, per essi infatti non viene allocata la memoria per la variabile puntata, operazione che è sempre a carico del programmatore, al contrario di quanto avviene con le variabili automatiche.

Sebbene sia possibile definire funzioni membro accessorie per l’assegnazione di un valore ad ogni dato membro di un oggetto, ripetere questa sequenza di istruzioni per ogni istanziazione sarebbe un’operazione tediosa e soggetta ad errori, e da rivedere ogni volta che si applica una modifica alla classe. L’uso di un costruttore appositamente definito, invece, consente di inizializzare tutti i membri della classe assieme, di modo che l’istanziazione di un oggetto sia sempre consistente con il nostro modello di dati e indipendente dalle scelte del compilatore.

Il listato seguente contiene la definizione della classe Point2D vista nella lezione precedente, cui sono stati aggiunti due costruttori:

// point2d.h
 
#ifndef POINT_2D_H
#define POINT_2D_H
 
namespace Geometry {
	class Point2D;      
}   
 
class Geometry::Point2D
{
public:
	Point2D(); 									// costruttore di default			
	Point2D(double xValue, double yValue);		// costruttore sovraccaricato

	double X();
	void setX(double value);
			 
	double Y();
	void setY(double value);            
			 
	double distanceFrom(Point2D other);
 
private:
	double x;
	double y;
};       
 
#endif // POINT_2D_H  

Il listato successivo ne contiene l’implementazione. Il costruttore di default inizializza, per nostra scelta, i valori di x e y a zero, mentre il costruttore sovraccaricato utilizza i metodi accessori per assegnare a x e y due valori arbitrari. In questo modo, anche per gli argomenti del costruttore varranno i medesimi vincoli che abbiamo applicato ai metodi setter per quanto riguarda i valori ammissibili come coordinate.

A parte che per la sua firma, infatti, il costruttore di una classe può essere usato come una funzione membro qualsiasi, e può eseguire qualsiasi tipo di istruzione, anche invocare altre funzioni. Tuttavia una buona pratica è quella di limitare il lavoro svolto dal costruttore alla semplice inizializzazione dei membri di un oggetto, e demandare ad altri metodi le altre funzionalità o comportamenti della classe.

// point2d.cpp
#include "point2d.h"
#include <cmath>
 
Geometry::Point2D::Point2D() 
{
	x = 0;
	y = 0;
}

Geometry::Point2D::Point2D(double xVal, double yVal)
{	
	setX(xVal);
	setY(yVal);
}		

void Geometry::Point2D::setX(double value) {
	if (!std::isnan(value) && !std::isinf(value))
		x = value;      
	else
		x = 0;
}
 
void Geometry::Point2D::setY(double value) {
	if (!std::isnan(value) && !std::isinf(value))
		y = value;      
	else
		y = 0;  
}

// altro...

Distruggere gli oggetti

Il distruttore di classe assolve il compito di rilasciare tutte le risorse associate ad un oggetto, quando esso non è più necessario.

Come per il costruttore, anche la firma del distruttore ha una sintassi particolare:

  • il nome del distruttore coincide con quello della classe preceduto dal carattere ~;
  • il distruttore non ha un valore di ritorno, nè parametri e pertanto non può essere sovraccaricato.

Per il resto, il distruttore è una funzione come le altre, in cui può essere eseguita qualsiasi istruzione. Tuttavia, il suo compito primario dovrebbe essere sempre e solo quella di rimuovere un oggetto e tutte le sue dipendenze dallo stato del programma in maniera sicura e completa.

Il distruttore viene invocato per tutte le variabili automatiche ogni volta che viene raggiunta la fine del loro ambito di visibilità e tutte le volte che le variabili dinamiche vengono deallocate con la parola chiave delete.

Come per il costruttore, se non viene esplicitamente incluso nella dichiarazione della classe, il distruttore viene generato automaticamente dal compilatore. In questo caso tuttavia, l’unica risorsa rilasciata è la memoria occupata dall’oggetto. Se alcuni dei suoi dati membro sono delle reference, o se l’oggetto fa uso di altre risorse (file o altre interfacce di I/O), potrebbe essere necessario svolgere delle operazioni per il loro corretto rilascio.

Nell’esempio precedente, la definizione della classe Point2D è tale da rendere sufficiente l’uso del distruttore generato automaticamente dal compilatore. Nell’esempio seguente consideriamo invece il caso in una classe, in cui il costruttore di default ed il distruttore hanno effettivamente il compito di allocare le risorse in fase di inizializzazione e di rilasciarle quando l’oggetto viene distrutto.

// a.h

class A {
public:
	A();
	~A();
 
private:
	int m;
	int *ptr;

};

// a.cpp

A::A()
{
	m = 0;
	ptr = new int;
}

A::~A()
{
	delete ptr;
}
		   

In questo caso, il distruttore rilascia la memoria associata al membro ptr. Se avessimo lasciato al compilatore la generazione del distruttore, ciò avrebbe determinato un memory leak, cioè l’accumulo di aree di memoria inutilizzabili fino alla terminazione del programma, per ogni distruzione di oggetti di tipo A.

A parte lo spreco di memoria, che può essere contenuto o rilevante, a seconda delle operazioni svolte dal nostro programma a tempo di esecuzione, la memoria allocata e non correttamente rilasciata ha come effetto collaterale quello di aggravare la frammentazione dello heap, cioè la mancanza di grandi blocchi di memoria libera. Tale fenomeno può infatti degradare le prestazioni del programma, anche in punti apparentemente non correlati con l’uso della classe da cui origina il leak vero e proprio, rendendo difficile l’individuazione del problema.

Pertanto occorre sempre prestare attenzione alla implementazione del distruttore della classe, evitando di demandare troppe responsabilità al compilatore.

Tutte le lezioni

1 ... 16 17 18 ... 66

Se vuoi aggiornamenti su Costruttori e distruttori inserisci la tua e-mail nel box qui sotto:
Tags:
 
X
Se vuoi aggiornamenti su Costruttori e distruttori

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy