Ottava lezione

stack vs. heap

Dover stimare e conoscere la dimensione della memoria richiesta dall'esecuzione di un programma al momento della compilazione è un requisito che appartiene definitivamente al passato.
Ci sono sostanzialmente due modi per ottenere memoria al momento dell'esecuzione di un programma: Le varie funzioni di stack e heap sono illustrate in questa figura:

Gestire l'immagazzinamento di oggetti e dati sia nello stack che nello heap è costoso, e richiede cautela. Esistono vari accorgimenti, anche intrinseci del linguaggio, per evitare operazioni inutili ed assicurarsi di non sprecare spazio.

References

Il C++ ha esteso il significato di operatori già definiti nel C:

Vediamo l'uso di questi operatori in questo semplicissimo , dove si mostra la possibilità di definire e trattare puntatori a qualsiasi oggetto come un vero e proprio tipo di dati. Una variabile di tipo puntatore può quindi non essere inizializzata a nulla, o contenere valori indefiniti, oppure puntare ad aree di memoria ricavate dello heap mediante un'operazione di allocazione, che può fallire.

Il C++ ha esteso il significato di questi operatori, affiancando, al tipo di dati "puntatore a oggetto" un tipo di dati "riferimento (reference) a oggetto". Si può così trasferire l'accesso a oggetti sicuramente esistenti fra funzioni senza generare nello stack copie di oggetti (con le relative creazioni e distruzioni) indesiderate e probabilmente non necessarie. L'uso di oggetti di cui sia disponibile una reference è identico a quelli generati localmente. La differenza fra il passaggio di oggetti alle funzioni per valore e per reference è illustrata da questo

La funzione chiamante ignora se una determinata funzione chiamata acceda ad un argomento utilizzando una copia del valore oppure utilizzando una reference. L'unico elemento che permette alla funzione chiamante di essere ragionevolmente certa che un oggetto non venga modificato dalla funzione chiamata è che l'argomento sia dichiarato const nel prototipo.
Come per ogni limitazione in C++, esistono costrutti che permettono di superare anche la dichiarazione di variabili costanti (come ad esempio const_cast<>) in casi speciali. Bisogna tuttavia assolutamente evitare l'utilizzo di tali costrutti in una funzione che dichiara argomenti costanti.

Pointers

Abbiamo accennato al fatto che si può in genere ottenere dinamicamente, durante l'esecuzione del programma, la disponibilità di aree di memoria di dimensione arbitraria: si tratta però di un'operazione che può fallire. Il suo risultato si adatta quindi male ad un tipo di dati, come reference, che deve puntare ad un oggetto esistente, già completamente e correttamente creato.

Un altro caso di operazione che può fallire e per la quale è necessario utilizzare un puntatore è illustrato in questo
Dovrebbe ora risultare più chiara anche la ragione per cui si utilizza l'operatore * per accedere al valore puntato dagli iteratori della STL.

Come detto, nonostante le capacità del C++ di allocare oggetti sullo stack siano piuttosto evolute (e spesso sufficienti), in alcuni casi può essere necessario disporre di aree di memoria persistenti rispetto al cambio di contesto. Oggetti dinamici di questo tipo si possono ottenere utilizzando l'operatore new:

// A measurement that needs to survive this function or this context:
measurement *long_lived_meas = 
    new measurement(3.141516, "31 Dec 1999", "Mino Cancelli");

L'unico vero problema degli oggetti ottenuti dallo heap è proprio la loro lunga durata: persistono finché non vengono distrutti (utilizzando l'operatore delete). Inoltre, anche in caso di termine del programma e del processo che lo esegue non viene invocato il relativo distruttore (cosa che può avere conseguenze indesiderate).
La dimenticanza di deallocare un oggetto allocato sullo heap può rendere la corrispondente quantità di memoria non più utilizzabile per la durata del processo. Questo fenomeno è descritto come memory leak.

L'uso indiscriminato di oggetti allocati sullo heap è una pratica da non incoraggiare. Se non si riesce a fare a meno di utilizzarli, si può fare in modo di non dover mai invocare esplicitamente l'operatore delete.

Smart Pointers

Le tecniche dei template e dell'overloading degli operatori sono state utilizzate per realizzare altri tipi di oggetti che si comportano come puntatori, ma hanno la caratteristica di invocare automaticamente la distruzione dell'oggetto puntato quando i vari contesti in cui viene utilizzato vengono chiusi. E' possibile così realizzare in modo sicuro:

Sono stati dunque sviluppati, da principio nel progetto BOOST (formato inizialmente da membri del comitato di standardizzazione del C++), e quindi integrati nello standard C++ 2011, due tipi di puntatori furbi:

  1. #include <memory>
    std::unique_ptr<measurement> meas(new measurement(3.141516, "31 Dec 1999", "Mino Cancelli"));
    Esempio
  2. #include <memory>
    std::shared_ptr<measurement> meas(new measurement(3.141516, "31 Dec 1999", "Mino Cancelli"));
    Esempio

Questi smart pointer possono essere utilizzati per rispondere a qualsiasi necessità di deallocazione degli oggetti che richiederebbe l'uso esplicito di delete. Considerati i cospicui danni recati all'umanità dalle memory leak nei primi anni di esistenza ed impiego del C++, questa pratica è ora fortemente raccomandabile.

Nota: nello standard precedente del C++ (1998) era definito un solo tipo di smart pointer, std::auto_ptr, che però richiedeva l'assegnazione manuale del controllo del puntatore in caso di condivisione o passaggio ad altre funzioni. Queste limitazioni pratiche sono superate, come appena visto, nella nuova versione dello standard.

Esercizio: Modificare il programma che disegna le sfere tridimensionali in modo che legga i dati di un numero arbitrario di sfere da un file di testo.