[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[ Seminari ] [ Seminari su CD ROM ] [ Consulenza]
[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
trad. italiana e adattamento a cura di Umberto Sorbo
A volte si conosce l'esatta quantità, tipo e tempo di vita degli oggetti del nostro programma. Ma non sempre.
Quanti aeroplani gestirà un sistema di traffico aereo? Quante figure userà un sistema CAD? Quanti nodi ci saranno in una rete?
Per risolvere il problema della programmazione generale, è essenziale che si possa creare e distruggere gli oggetti a tempo di esecuzione. Naturalmente, il C ha sempre fornito funzioni per l'allocazione dinamica della memoria malloc() e free() ( insieme con le varianti di malloc()) che allocano memoria nella heap ( anche detta memoria libera ) a runtime.
Tuttavia, queste non funzioneranno in C++. Il costruttore non permette di maneggiare l'indirizzo della memoria da inizializzare e per una buona ragione. Se lo si potesse fare, si potrebbe:
Dimenticare. L'inizializzazione garantita degli oggetti in C++ non sarebbe tale.
Accidentalmente fare qualcosa all'oggetto prima di inizializzarlo, aspettandosi che accada la cosa giusta.
Maneggiare l'oggetto di dimensioni sbagliate.
E naturalmente, persino se si è fatto tutto correttamente, chiunque modificasse il nostro programma è incline agli stessi errori. L'impropria inizializzazione è responsabile di un gran numero di problemi della programmazione, perciò è molto importante garantire le chiamate ai costruttori per gli oggetti creati nella heap.
Perciò come fa il C++ a garantire una corretta inizializzazione e pulizia, ma permettere di creare oggetti dinamicamente nella heap?
La risposta è di portare la creazione dinamica nel nucleo del linguaggio malloc( ) e free() sono funzioni di libreria e così fuori dal controllo del compilatore. Tuttavia, se si ha un operatore per eseguire l'atto combinato dell'allocazione dinamica della memoria e l'inizializzazione ed un altro operatore per eseguire l'atto combinato di pulizia e rilascio della memoria, il compilatore può ancora garantire che i costruttori e distruttori saranno chiamati per tutti gli oggetti.
In questo capitolo, si imparerà come le funzioni new e delete del C++ elegantemente risolvono questo problema, creando in modo sicuro gli oggetti nella heap.
Quando un oggetto C++ viene creato accadono due eventi:
La memoria è allocata per l'oggetto.
Il costruttore viene chiamato per inizializzare la memoria.
Per ora si dovrebbe credere che il passo due ci sia sempre. Il C++ lo impone perchè oggetti non inizializzati sono la maggior fonte di bachi. Non importa dove o come l'oggetto viene creato, il costruttore viene sempre chiamato.
Tuttavia, può succedere che il passo uno venga attuato in diversi modi o in tempi alterni:
La memoria può essere allocata nell'area di memoria statica prima che il programma cominci. Questa memoria esiste durante tutta la vita del programma.
Lo spazio può essere creato nello stack ogni volta che viene raggiunto un particolare punto di esecuzione ( una parentesi aperta). Questo spazio viene rilasciato automaticamente in un punto complementare di esecuzione ( la parentesi di chiusura). Queste operazioni di allocazione nello stack vengono eseguite con le istruzioni proprie del processore e sono molto efficienti. Tuttavia, si deve sapere esattamente di quante variabili si ha bisogno quando si scrive il programma in modo che il compilatore possa generare il giusto codice.
Lo spazio può essere allocato da una zona di memoria detta heap ( anche conosciuta come memoria libera). Ciò viene detta allocazione dinamica della memoria. Per allocare questa memoria, viene chiamata una funzione a run time (tempo di esecuzione); ciò significa che si può decidere in qualsiasi momento che si vuole della memoria e quanta se ne vuole. Si è anche responsabili del rilascio della memoria, ciò significa che il tempo di vita della memoria è a piacere e non dipende dallo scope.
Spesso queste tre regioni sono piazzate in un singolo pezzo contiguo di memoria fisica: l'area statica, lo stack e la heap ( in un ordine determinato dal progettista del compilatore). Tuttavia, non ci sono regole. Lo stack può essere in un posto speciale e la heap può essere implementato facendo chiamate a pezzi di memoria dal sistema operativo. Per un programmatore, queste cose sono normalmente nascoste, quindi tutto ciò che bisogna sapere è che la memoria è disponibile quando serve.
Per allocare memoria dinamicamente a runtime, il C fornisce funzioni nella sua libreria standard: malloc() e le sue varianti calloc() e realloc() per allocare memoria nella heap, free() per rilasciare la memoria alla heap. Queste funzioni sono pragmatiche ma primitive e richiedono comprensione ed attenzione da parte del programmatore. Per creare un'istanza di una classe nella heap usando le funzioni della memoria dinamica del C, si dovrebbe fare qualcosa del genere:
//: C13:MallocClass.cpp
// Malloc con oggetti di classi
// Cosa si deve fare se non si vuole usare "new"
#include "../require.h"
#include <cstdlib> // malloc() & free()
#include <cstring> // memset()
#include <iostream>
using namespace std;
class Oggetto {
int i, j, k;
enum { sz = 100 };
char buf[sz];
public:
void inizializza() { // non si può usare il costruttore
cout << "inizializzo Oggetto" << endl;
i = j = k = 0;
memset(buf, 0, sz);
}
void distruggi() const { // Non si può usare il distruttore
cout << "distruggo Oggetto" << endl;
}
};
int main() {
Oggetto* oggetto = (Oggetto*)malloc(sizeof(Oggetto));
require(oggetto != 0);
oggetto->inizializza();
// ... un pò dopo:
oggetto->distruggi();
free(oggetto);
} ///:~
Si può vedere l'uso di malloc() per creare spazio per l'oggetto nella linea:
Oggetto* oggetto = (Oggetto*)malloc(sizeof(Oggetto));
Qui, l'utente deve determinare le dimensioni dell'oggetto (un posto per un errore). malloc() restituisce un void* perchè produce solo una porzione di memoria, non un oggetto. Il C++ non permette di assegnare un puntatore void* ad un qualsiasi altro puntatore, perciò bisogna effettuare un cast.
Poichè malloc() può fallire nel ricercare memoria libera ( in questo caso restituisce zero), si deve controllare il puntatore restituito per essere sicuri che tutto sia andato bene.
Ma il problema peggiore è questa linea:
oggetto->inizializza();
Gli utenti devono ricordasi di inizializzare l'oggetto prima di usarlo. Si noti che non è stato usato un costruttore perchè il costruttore non può essere chiamato esplicitamente[50] , esso viene chiamato per noi dal compilatore quando viene creato un oggetto . Il problema qui è che l'utente ora ha la possibilità di dimenticare di eseguire l'inizializzazione prima che l'oggetto venga utilizzato, introducendo così una grossa sorgente di bachi.
Molti programmatori trovano che le funzioni per l'allocazione dinamica della memoria in C sono molto complicate e fonte di confusione; è comune trovare programmatori C che usano macchine di memoria virtuale che allocano enormi array di variabili nell'area statica di memoria per evitare di pensare all'allocazione dinamica della memoria. Poichè il C++ sta cercando di rendere sicure e facili da usare le librerie per il programmatore inesperto, l'approccio del C alla memoria dinamica è inaccettabile.
La soluzione del C++ è quella di combinare tutte le azioni necessarie per creare un oggetto in un singolo operatore chiamato new. Quando si crea un oggetto con new, esso alloca sufficiente memoria nella heap per contenere l'oggetto e chiama il costruttore per quella memoria. Cosi , se si scrive:
MioTipo *fp = new MioTipo(1,2);
a runtime, viene chiamato l'equivalente di malloc(sizeof(MioTipo)) (spesso, è una chiamata a malloc()) ed il costruttore per MioTipo viene chiamato con l'indirizzo risultante come il puntatore this, usando (1,2) come lista argomenti. Il puntatore è poi assegnato a fp, esso è un oggetto istanziato ed inizializzato, non ci si può mettere le mani sopra prima di allora. Esso è anche il giusto tipo MioTipo perciò non è necessario il cast.
La new di default controlla se l'allocazione della memoria è avvenuta con successo prima di passare l'indirizzo al costruttore, perciò non si deve esplicitamente determinare se la chiamata ha avuto successo. Più avanti nel capitolo si scoprirà cosa accade se non c'è più memoria libera.
Si può creare una nuova espressione usando qualsiasi costruttore disponibile per la classe. Se il costruttore non ha argomenti, si scrive l'espressione new senza la lista di argomenti del costruttore:
MioTipo *fp = new MioTipo;
Si noti come il processo di creazione degli oggetti nella heap diventi semplicemente una singola espressione, con tutti i controlli sulla dimensione, conversione e sicurezza integrati dentro. È facile creare un oggetto nella heap come se lo si facesse nello stack.
Il complemento alla espressione new è l'espressione delete, che per prima chiama il distruttore e poi libera la memoria ( spesso con una chiamata alla free() ). Proprio come una espressione new ritorna un puntatore all'oggetto, un' espressione di delete richiede l'indirizzo di un oggetto.
delete fp;
Ciò distrugge e poi libera la memoria allocata per l'oggetto MioTipo creato dinamicamente precedentemente.
delete può essere chiamata solo per un oggetto creato con new. Se si usa malloc( ) (o calloc( ) o realloc( )) e poi delete, il comportamento non è definito. Poichè molte implementazioni di new e delete usano malloc() e free(), si terminerà con il rilascio della memoria senza chiamare il distruttore.
Se il puntatore che si sta cancellando è zero, non accadrà niente. Per questo motivo, spesso si raccomanda di impostare il puntatore a zero immediatamente dopo che lo si è cancellato, in modo da prevenire la cancellazione doppia. Cancellare un oggetto più di una volta è una cosa completamente sbagliata e causa problemi.
Questo semplice esempio mostra come avviene l'inizializzazione:
//: C13:Tree.h
#ifndef TREE_H
#define TREE_H
#include <iostream>
class Albero {
int altezza;
public:
Albero(int altezzaAlbero) : altezza(altezzaAlbero) {}
~Albero() { std::cout << "*"; }
friend std::ostream&
operator<<(std::ostream& os, const Albero* t) {
return os << " altezza Albero:"
<< t->altezza<< std::endl;
}
};
#endif // TREE_H ///:~
//: C13:NewAndDelete.cpp
// semplice demo di new & delete
#include "Tree.h"
using namespace std;
int main() {
Albero* t = new Albero(40);
cout << t;
delete t;
} ///:~
La prova che il costruttore viene chiamato è data dalla stampa del valore di Albero. Qui , viene fatto con l'overloading dell'operatore << per usarlo con un ostream ed un Albero*. Si noti, tuttavia, che anche se la funzione è dichiarata come un friend, essa è definita come una inline! Ciò è per pura convenienza, definire una funzione friend come inline per una classe non cambia lo stato di friend o il fatto che essa è una funzione globale e non una funzione membro della classe. Si noti anche che il valore di ritorno è il risultato di un' intera espressione, che è un ostream& ( così deve essere, per soddisfare il tipo del valore di ritorno della funzione).
Quando si creano oggetti automatici nello stack, la dimensione degli oggetti ed il loro tempo di vita è costruito direttamente nel codice generato, perchè il compilatore conosce esattamente tipo, quantità e visibilità. Creare oggetti nella heap implica maggior overhead, sia come tempo che come spazio. Ecco un tipico scenario. (Si può rimpiazzare malloc() con calloc() o realloc () )
Si chiama malloc(), che richiede un blocco di memoria libera. ( Questo codice potrebbe essere in realtà parte di malloc() )
Viene cercato un blocco di memoria libera grande abbastanza per soddisfare la richiesta. Ciò avviene controllando una mappa che mostra quali blocchi sono correntemente in uso e quali sono disponibili. È un processo rapido, ma può richiedere più di un tentativo quindi potrebbe non essere deterministico, cioè non si può pensare che malloc() richieda sempre la stessa quantità di tempo.
Prima che un puntatore al blocco venga restituito, la dimensione e la locazione del blocco deve essere memorizzato in modo che successive chiamate di malloc() non lo usino e che quando si chiama free(), il sistema sappia quanta memoria liberare.
Il modo in cui tutto ciò viene implementato può variare enormemente. Per esempio, non c'è niente che impedisce che le primitive per l'allocazione della memoria siano implementate nel processore. Se si è curiosi, si può scrivere dei programmi di test per provare a indovinare il modo in cui malloc() è implementata. Si può anche leggere la libreria del codice sorgente, se la si ha ( I sorgenti del C GNU sono sempre disponibili).
Utilizzando new e delete, l'esempio Stash introdotto precedentemente in questo libro può essere riscritto usando tutte le caratteristiche discusse nel libro finora. Esaminando il nuovo codice si avrà anche una utile rassegna degli argomenti.
A questo punto del libro, nè la classe Stash che la Stack possederanno gli oggetti a cui puntano; cioè, quando l'oggetto Stash o Stack perde visibilità, non chiamerà delete per tutti gli oggetti a cui punta. La ragione per cui ciò non è possibile è data dal fatto che, in un tentativo di essere generici, essi gestiscono puntatori void. Se si cancella un puntatore void, la sola cosa che accade è che la memoria viene rilasciata, perchè non c'è nessuna informazione sul tipo e non c'è modo per il compilatore di sapere quale distruttore chiamare.
Vale la pena di chiarire che se si chiama delete per un void*, è quasi certo che si tratta di bug nel nostro programma a meno che la destinazione del puntatore sia molto semplice; in particolare, si dovrebbe non avere un distruttore. Qui c'è un esempio per mostrare cosa accade:
//: C13:BadVoidPointerDeletion.cpp
// Deleting void pointers can cause memory leaks
#include <iostream>
using namespace std;
class Object {
void* data; // un pò di memoria
const int size;
const char id;
public: Object(int sz, char c) : size(sz), id(c) {
data = new char[size];
cout << "Costruzione oggetto" << id << ", dimensioni = " << size << endl;
}
~Object() {
cout << "Distruzione oggetto" << id << endl;
delete []data; // OK, rilascio solo la memoria
// non è necessaria la chiamata al distruttore
}
};
int main() {
Object* a = new Object(40, 'a');
delete a;
void* b = new Object(40, 'b');
delete b;
} ///:~
La classe Object contiene un void* che è inizializzato da dati "grezzi" ( non punta ad oggetti che hanno un distruttore). Nel distruttore di Object, delete viene chiamato per questo void* senza effetti indesiderati, poichè l'unica cosa di cui abbiamo bisogno che accada è che sia rilasciata la memoria.
Tuttavia, nel main() si può vedere che è indispensabile che delete sappia con che tipo di oggetto sta lavorando. Questo è l'output:
Costruzione oggetto a, dimensione = 40
Distruzione oggetto a
Costruzione oggetto b, dimensione = 40
Poichè delete a sa che a punta ad un Object, il distruttore viene chiamato e così la memoria allocata per data viene liberata. Tuttavia, se si manipola un oggetto tramite un void* come nel caso di delete b, l'unica cosa che accade è che la memoria di Object viene liberata, ma il distruttore non viene chiamato quindi non c'è memoria rilasciata a cui data punta. Quando questo programma viene compilato, probabilmente non si vedranno messaggi di warning; il compilatore assume che si sappia cosa si sta facendo. Si ottiene quello che in gergo viene detto un memory leak ( una falla di memoria).
Se si ha un memory leak nel programma, si ricerchino tutti i delete controllando il tipo di puntatore che si sta cancellando. Se è un void* allora si è trovata probabilmente una sorgente del memory leak (tuttavia il C++ fornisce altre ampie opportunità per i memory leak ).
Per rendere flessibili i contenitori Stash e Stack ( capaci di gestire qualsiasi tipo di oggetto), essi gestiranno puntatori void. Ciò significa che quanto viene restituito un puntatore dall'oggetto Stash o Stack, si deve fare il cast di esso al tipo opportuno prima di usarlo; come abbiamo visto sopra, si deve usare il cast verso il tipo opportuno prima di cancellarlo o si avrà un memory leak.
Gli altri casi di memory leak hanno a che fare con la sicurezza che quel delete è realmente chiamato per ogni puntatore ad oggetto gestito nel contenitore. Il contenitore non può possedere un puntatore perchè lo gestisce come un void* e quindi non può eseguire la corretta pulizia. L'utente ha la responsabilità della pulizia degli oggetti. Ciò produce una serie di problemi se si aggiungono puntatori agli oggetti creati nello stack ed oggetti creati nella heap dallo stesso contenitore perchè l'espressione delete non è sicura per un puntatore che non era stato allocato nella heap ( e quando si prende un puntatore dal container, come facciamo a sapere dove è stato allocato il suo oggetto?). Quindi, si deve essere sicuri che gli oggetti memorizzati nelle versioni che seguono di Stash e Stack vengano fatti solo nella heap, sia tramite una attenta programmazione o creando classi che possono solo essere allocate nella heap.
È anche importante essere sicuri che il programmatore client si prenda la responsabilità di pulizia di tutti i puntatori nel container. Si è visto negli esempi precedenti come la classe Stack controlla nel suo distruttore che tutti gli oggetti Link siano stati poppati. Per uno Stash di puntatori, tuttavia, c'è bisogno di un altro approccio.
Questa nuova versione della classe Stash, chiamato PStash gestisce puntatori ad oggetti che esistono di per sè nella heap, mentre la vecchia Stash nei capitoli precedenti copiava gli oggetti per valore nel container Stash. Usando new e delete, è facile e sicuro gestire puntatori ad oggetti che sono stati creati nella heap.
Qui c'è l'header file per il puntatore Stash:
//: C13:PStash.h
// Gestisce puntatori invece di oggetti
#ifndef PSTASH_H
#define PSTASH_H
class PStash {
int quantity; // Numero di spazio libero
int next; // prossimo spazio libero
// puntatore allo spazio:
void** storage;
void inflate(int increase);
public:
PStash() : quantity(0), storage(0), next(0) {}
~PStash();
int add(void* element);
void* operator[](int index) const; // accesso
// rimuove il riferimento da questo PStash:
void* remove(int index);
// Numero di elementi in Stash:
int count() const { return next; }
};
#endif // PSTASH_H ///:~
I sottostanti elementi dei dati sono simili, ma ora storage è un array di puntatori void e l'allocazione dello spazio per questo array è eseguita con new invece di malloc(). Nell'espressione
void** st = new void*[quantity + increase];
il tipo di oggetto allocato è un void*, quindi l'espressione alloca un array di puntatori void.
Il distruttore elimina lo spazio dove sono tenuti i puntatori void piuttosto che tentare di cancellare ciò che essi puntano ( che, come precedentemente notato, rilascerà la loro memoria e non chiamerà il distruttore perchè un puntatore void non ha informazione di tipo).
L'altro cambiamento è operator[] con fetch() , che ha più senso sintatticamente. Di nuovo, tuttavia, viene restituito un void*, quindi l'utente deve ricordarsi quali tipi sono memorizzati nel container ed eseguire un cast sui puntatori quando li usa ( un problema che sarà risolto nei prossimi capitoli).
Qui ci sono le definizioni delle funzioni membro:
//: C13:PStash.cpp {O}
// definizioni Pointer Stash
#include "PStash.h"
#include "../require.h"
#include <iostream>
#include <cstring> // funzioni 'mem'
using namespace std;
int PStash::add(void* element) {
const int inflateSize = 10;
if(next>= quantity)
inflate(inflateSize);
spazio[next++] = element;
return(next - 1); // numero indice
}
// No ownership:
PStash::~PStash() {
for(int i = 0; i < next; i++)
require(storage[i] == 0,
"PStash non ripulito");
delete []storage;
}
// overloading Operatore per rimpiazzare fetch
void* PStash::operator[](int index) const {
require(index>= 0,
"PStash::operator[] indice negativo");
if(index>= next)
return 0; // Per indicare la fine
// produce un puntatore all'elemento desiderato:
return storage[index];
}
void* PStash::remove(int index) {
void* v = operator[](index);
// "Rimuove" il puntatore:
if(v != 0) storage[index] = 0;
return v;
}
void PStash::inflate(int increase) {
const int psz = sizeof(void*);
void** st = new void*[quantity + increase];
memset(st, 0, (quantity + increase) * psz);
memcpy(st, storage, quantity * psz);
quantity += increase;
delete []storage; // Vecchio spazio
storage = st; // Punta a memoria nuova
} ///:~
La funzione add() è la stessa di prima, tranne che è memorizzato un puntatore invece della copia di un intero oggetto.
Il codice inflate() è stato modificato per gestire l'allocazione di un array di void*, mentre nella versione precedente funzionava solo con byte grezzi. Qui, invece di usare l'approccio di copiare con un indice di array, viene usata prima la funzione della libreria Standard C memset() per impostare a zero la memoria nuova (ciò non è strettamente necessario, poichè PStash sta presumibilmente gestendo la memoria correttamente, ma di solito non fa male un bit di attenzione in più). Poi memcpy() muove i dati esistenti dalla vecchia locazione alla nuova. Spesso, funzioni come memset() e memcpy() sono state ottimizzate nel tempo, quindi possono essere più veloci dei loop mostrati precedentemente. Ma con una funzione come inflate() che probabilmente non sarà usata spesso, si però non vedere questa differenza di performance. Tuttavia, il fatto che le chiamate a funzione sono più concise dei loop possono aiutare a prevenire errori nel codice.
Per addossare la responsabilità della pulizia degli oggetti sulle spalle del programmatore client, ci sono due modi di accedere ai puntatori in PStash: l'operator[], che semplicemente restituisce il puntatore ma lo lascia come un membro del container ed una seconda funzione membro remove(), che restituisce il puntatore ma lo rimuove anche dal container assegnando quella posizione a zero. Quando viene chiamato il distruttore per PStash, esso controlla per essere sicuro che tutti i puntatori agli oggetti sono stati rimossi; in caso contrario, si è avvertiti in modo che si può prevenire un memory leak ( le soluzioni più eleganti sono presenti negli ultimi capitoli).
ecco il vecchio programma di test per Stash riscritto per PStash:
//: C13:PStashTest.cpp
//{L} PStash
// Test per puntatore Stash
#include "PStash.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
PStash stashIntero;
// 'new' funziona anche con tipi predefiniti. Si noti
// la sintassi "pseudo-costruttore":
for(int i = 0; i < 25; i++)
stashIntero.add(new int(i));
for(int j = 0; j < stashIntero.count(); j++)
cout << "stashIntero[" << j << "] = "
<< *(int*)stashIntero[j] << endl;
// pulizia:
for(int k = 0; k < stashIntero.count(); k++)
delete stashIntero.remove(k);
ifstream in ("PStashTest.cpp");
assure(in, "PStashTest.cpp");
PStash stashStringa;
string linea;
while(getline(in, linea))
stashStringa.add(new string(linea));
// Stampa le stringhe:
for(int u = 0; stashStringa[u]; u++)
cout << "stashStringa[" << u << "] = "
<< *(string*)stashStringa[u] << endl;
// Pulizia:
for(int v = 0; v < stashStringa.count(); v++)
delete (string*)stashStringa.remove(v);
} ///:~
Come prima, gli Stash vengono creati ed in essi memorizzata l'informazione, ma quest'ultima è il puntatore che si ottiene dalle new. Nel primo caso, si noti la linea:
stashIntero.add(new int(i));
L'espressione new int(i) usa la pseudo forma del costruttore, quindi la memoria per un nuovo oggetto int viene creata nella heap e l'int è inizializzato al valore i.
Durante la stampa, il valore restituito dall'operatore di PStash::operator[ ] deve essere castato al tipo opportuno; ciò viene ripetuto per i restanti oggetti PStash del programma. Non è un effetto desiderato usare puntatori void e ciò verrà aggiustato nei prossimi capitoli.
Il secondo test apre i file sorgenti e legge una linea alla volta dentro un altro PStash. Ogni linea viene letta dentro una string usando getline(), poi una nuova stringa viene creata da linea per fare una copia indipendente di quella linea. Se passiamo solo l'indirizzo di linea ogni volta, avremo un mucchio di puntatori a linea, che conterrebbero solo l'ultima linea che è stata letta dal file.
Quando si usa il puntatore si vede l'espressione:
*(string*) stashStringa[v]
Il puntatore ritornato dall'operatore [] deve essere castato ad un string* per aver il giusto tipo. Poi la string * è dereferenziata in modo da valutare l'espressione ad un oggetto, al punto in cui il compilatore vede un oggetto string e lo manda a cout.
L'oggetto creato nella heap deve essere distrutto con l'uso del comando remove() o altrimenti si avrà un messaggio a runtime che ci dice che non sono stati puliti completamente gli oggetti in PStash. Si noti che nel caso di puntatori a int, nessun cast è necessario perchè non c'è distruttore per un int e tutto ciò di cui abbiamo bisogno è rilasciare la memoria:
delete stashIntero.remove(k);
Tuttavia, per i puntatori string, se si dimentica di fare il cast si avrà un altro memory leak, quindi il cast è essenziale:
delete (string*)
stringStash.remove(k);
Alcuni di questi problemi ( ma non tutti ) possono essere eliminati usanto i template ( che si impareranno ad usare nel Capitolo 16).
Nel C++, si possono creare array di oggetti nello stack o nella heap con uguale facilità, e (naturalmente) il costruttore viene chiamato per ogni oggetto dell'array. C'è un vincolo, tuttavia: ci deve essere un costruttore di default, tranne per l'inizializzazione dell'aggregato sullo stack ( si veda il Capitolo 6), perchè un costruttore con nessun argomento deve essere chiamato per ogni oggetto.
Quando si creano array di oggetti sulla heap usando new, c'è qualcosa altro che si deve fare. Un esempio di tale array è
MioTipo* fp = new MioTipo[100];
Questo alloca sufficiente spazio nella heap per 100 oggetti MioTipo e chiama il costruttore per ognuno. Ora, tuttavia, si ha semplicemente un MioTipo*, che è esattamente lo stesso che si avrebbe se si avesse scritto
MioTipo* fp2 = new MioTipo;
per creare un singolo oggetto. Poichè noi abbiamo scritto il codice, sappiamo che fp è realmente l'indirizzo di partenza di un array, quindi ha senso selezionare gli elementi dell'array usando un espressione tipo fp[3]. Ma cosa accade quando si distrugge l'array? Le due righe
delete fp2; // OK
delete fp; // non si ottiene l'effetto desiderato
sembrano esattamente le stesse e i loro effetti saranno gli stessi. Il distruttore sarà chiamato per l'oggetto MioTipo puntato dall'indirizzo dato e poi la memoria sarà rilasciata. Per fp2 ciò va bene, ma per fp ciò significa che le altre 99 chiamate al distruttore non saranno fatte. L'esatto totale della memoria sarà ancora liberato, tuttavia, poichè è allocato in un grosso pezzo e la dimensione dell'intero pezzo è conservata da qualche parte dalla routine di allocazione.
La soluzione richiede che si dia al compilatore l'informazione che questo è in realtà l'indirizzo di partenza di un array. Questo viene fatto con la seguente sintassi:
delete []fp;
Le parentesi vuote dicono al compilatore di generare codice che prende il numero di oggetto dell'array, memorizzati da qualche parte quando l'array viene creato e chiama il distruttore tante volte quanti sono gli oggetti. Questa è realmente una sintassi migliorata dalla forma precedente, che si può ancora vedere occasionalmente nei vecchi sorgenti:
delete [100]fp;
che obbliga il programmatore ad includere il numero di oggetti dell' array ed ad introdurre la possibilità che il programmatore commetta un errore. L'overhead aggiuntivo per il compilatore è molto basso ed è stato preferito specificare il numero di oggetti in un posto invece che in due.
Come digressione, fp definito sopra può puntare a qualsiasi cosa, che non ha senso per l'indirizzo di partenza di un array. Ha più senso definirlo come una costante , in modo che qualsiasi tentativo di modificare il puntatore sarà segnalato da un errore. Per ottenere questo effetto, si può provare
int const* q = new int[10];
oppure
const int* q = new int[10];
ma in entrambi i casi il const sarà vincolato all' int, cioè, ciò a cui si punta, piuttosto che la qualità del puntatore stesso. Invece, si deve scrivere:
int* const q = new int[10];
Ora gli elementi dell'array in q possono essere modificati, ma qualsiasi cambiamento a q ( tipo q++) è illegale, come con un ordinario identificatore di array.
Cosa accade quando l'operatore new() non trova un blocco contiguo di memoria largo abbastanza per mantenere l'oggetto desiderato? Una speciale funzione chiamata gestore del new viene chiamato. O piuttosto, un puntatore ad una funzione viene controllato e se il puntatore non è zero, allora la funzione a cui esso punta viene chiamata.
Il comportamento di default per il gestore del new è di lanciare un'eccezione, un argomento trattato nel Volume 2. Tuttavia, se si sta usando l'allocazione nella heap nel nostro programma, è saggio almeno rimpiazzare il gestore del new con un messaggio che dice che si è terminata la memoria e termina il programma. In questo modo, durante il debugging, si avrà un indizio di ciò che è successo. Per il programma finale si avrà bisogno di usare un recupero più robusto.
Si rimpiazza il gestore del new includendo new.h e chiamando poi set_new_handler( ) con l'indirizzo della funzione che si vuole :
//: C13:NewHandler.cpp
// Cambiare il gestore del new
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;
int conteggio = 0;
void fine_della_memoria() {
cerr << "memoria esaurita dopo" << conteggio
<< " allocazioni!" << endl;
exit(1);
}
int main() {
set_new_handler(fine_della_memoria);
while(1) {
conteggio++;
new int[1000]; // esaurisce la memoria
}
} ///:~
La funzione gestore del new deve non avere argomenti ed avere un valore di ritorno void, il ciclo while manterrà allocati oggetti int ( e getterà via i loro indirizzi di ritorno) fino a che la memoria libera viene esaurita. Alla prossima chiamata di new, non può essere allocata memoria, quindi il gestore del new verrà chiamato.
Il comportamento del gestore del new è legato all'operatore new(), si si vuole fare l'overload di new () ( spiegato nella prossima sezione) il gestore del new non sarà chiamato per default. Se si vuole ancora chiamare il gestore del new si deve scrivere il codice per fare ciò dentro operatore new sovraccaricato.
Naturalmente, si possono scrivere gestori del new più sofisticati, persino uno che cerca di recuperare memoria ( comunemente conosciuto come garbage collector). Questo non è un compito per programmatori novizi.
Quando si usa new, accadono due cose. Primo, la memoria viene allocata usando l'operatore new(), poi viene chiamato il costruttore. Con delete, viene chiamato distruttore, la memoria viene deallocata usando l'operatore delete(). Le chiamate al costruttore e distruttore non sono mai sotto il proprio controllo ( altrimenti le si potrebbero sovvertire accidentalmente), ma si possono cambiare le funzioni di allocazione della memoria new() e delete().
Il sistema di allocazione della memoria usato da new e delete è progettato per scopi generali. In situazioni speciali, tuttavia, non soddisfa i propri bisogni. Il motivo più comune per cambiare l'allocatore è l'efficienza: si potrebbe aver creato e distrutto così tanti oggetti di una particolare classe che è diventato un collo di bottiglia per la velocità. Il C++ permette di utilizzare l'overload per new e delete per implementare il proprio meccanismo di allocazione della memoria, quindi si possono gestire problemi di questo genere.
Un altro problema è la frammentazione della heap. Allocando oggetti di dimensioni diverse è possibile frazionare la heap al punto che effettivamente si finisce lo spazio. Cioè, la memoria potrebbe essere disponibile, ma a causa della frammentazione nessun pezzo è grande abbastanza per soddisfare le proprie esigenze. Creando il proprio allocatore per una particolare classe, si garantisce che ciò non accada mai.
Nei sistemi embedded e real-time, un programma può dover essere eseguito per un lungo periodo di tempo con risorse ristrette. Tale sistema può anche richiedere che l'allocazione della memoria prenda sempre lo stesso tempo e non sia permessa la mancanza o frammentazione di memoria. Un allocatore di memoria custom è la soluzione; altrimenti, i programmatori evitano del tutto di usare new e delete in tali casi perdendo un elemento prezioso del C++.
Quando si utilizzano gli operatori new() e delete() sovraccaricati è importante ricordare che si sta cambiando solo il modo in cui la memoria grezza viene allocata. Il compilatore chiamerà semplicemente la nostra new invece di quella di default per allocare memoria, poi chiamerà il costruttore per quella memoria. Quindi, sebbene il compilatore alloca memoria e chiama il costruttore quando vede una new, tutto ciò che si può cambiare quando si utilizza l'overload di new è la parte di allocazione della memoria. ( delete ha una limitazione simile).
Quando si utilizza l'overload di new(), si può anche rimpiazzare il comportamento quando si esaurisce la memoria, quindi si deve decidere cosa fare nell'operatore new(): restituire zero, scrivere un ciclo per chiamare il gestore di new e ritentare l'allocazione o (tipicamente) lanciare un eccezione di bad_alloc ( discussa nel Volume 2)
L'overload di new e delete è identico all'overload di qualsiasi altro operatore. Tuttavia, si ha la possibilità di fare l'overload dell'allocatore globale oppure utilizzare un allocatore differente per una particolare classe.
Questo è un approccio drastico, utilizzato quando le versioni globali di new e delete non soddisfano l'intero sistema. Se si sovraccaricano le versioni globali, si rendono le versioni di default completamente inaccessibili e non le si possono chiamare nemmeno dentro le proprie ridefinizioni.
La versione new sovraccaricata prende come argomento size_t ( il tipo standard del C Standard per le dimensioni). Questo argomento viene generato e passato a noi dal compilatore ed è la dimensione dell'oggetto di cui si responsabile per l'allocazione. Si deve restituire un puntatore ad un oggetto di quella dimensione ( o più grande , se si ha una ragione di farlo) o a zero se non c'è memoria libera ( in tal caso il costruttore non viene chiamato!). Tuttavia, se non c'è memoria, si dovrebbe fare qualcosa in più che restituire solo zero, tipo chiamare il gestore del new o lanciare un’eccezione per segnalare che c'è un problema.
Il valore di ritorno dell'operatore new() è un void*, non un puntatore ad un tipo particolare. Tutto ciò che si fa è produrre memoria, non un oggetto finito, ciò non accade finchè il costruttore non viene chiamato, un atto che il compilatore garantisce e che è fuori dal nostro controllo.
L'operatore delete() prende un void* alla memoria che è stata allocata dall'operatore new. È un void* perchè l'operatore delete ottiene un puntatore solo dopo che è stato chiamato il distruttore, che rimuove l'oggetto dalla memoria. Il tipo di ritorno è void.
Ecco un esempio che mostra come sovraccaricare le versioni globali di new e delete:
//: C13:GlobalOperatorNew.cpp
// Overload globale di new/delete
#include <cstdio>
#include <cstdlib>
using namespace std;
void* operator new(size_t sz) {
printf("operatore new: %d Bytes\n", sz);
void* m = malloc(sz);
if(!m) puts("fine della memoria);
return m;
}
void operator delete(void* m) {
puts("operatore delete");
free(m);
}
class S {
int i[100];
public:
S() { puts("S::S()"); }
~S() { puts("S::~S()"); }
};
int main() {
puts("creazione & distruzione di un int");
int* p = new int(47);
delete p;
puts("creazione & distruzione di un s");
S* s = new S;
delete s;
puts("creazione & distruzione S[3]");
S* sa = new S[3];
delete []sa;
} ///:~
Qui si può vedere la forma generale dell'overload di new e delete. Esse usano le funzioni del C Standard malloc() e free() per gli allocatori ( che usano per default anche new e delete!). Tuttavia, esse stampano anche messaggi di ciò che stanno facendo. Si noti che printf() e puts() vengono usate al posto di iostreams, perchè quando un oggetto iostream viene creato ( come cin, cout e cerr), esso chiama new per allocare memoria. Con printf(), non si incorre in un punto morto perchè non chiama new per inizializzarsi.
Nel main(), gli oggetti dei tipi predefiniti vengono creati per provare che new e delete sovraccaricati vengono chiamati anche in quel caso. Poi un singolo oggetto di tipo S viene creato, seguito da un array di S. Per l'array, si vedrà dal numero dei byte richiesti che memoria aggiuntiva viene allocata per memorizzare ( dentro l'array) il numero di oggetti gestiti. In tutti i casi vengono usate le versioni globali di new e delete sovraccaricate.
Sebbene non si deve esplicitamente usare static, quando si sovraccarica new e delete per una classe, si stanno creando funzioni membro statiche. Come prima, la sintassi è la stessa usata per gli operatori. Quando il compilatore vede che si usa new per creare un oggetto della propria classe, esso sceglie il membro operatore new() al posto della versione globale. Tuttavia, le versioni globali di new e delete vengono usate per tutti gli altri tipi di oggetti ( a meno che essi non hanno i loro new e delete).
Nel esempio seguente, viene creato un semplice sistema di allocazione della memoria per la classe Framis. Un pezzo di memoria è impostato a parte nell'area di memoria statica alla partenza del programma e quella memoria viene usata per allocare spazio per gli oggetti di tipo Framis. Per determinare quali blocchi sono stati allocati, viene usato un semplice array, un byte per ogni blocco:
//: C13:Framis.cpp
// Local overloaded new & delete
#include <cstddef> // Size_t
#include <fstream>
#include <iostream>
#include <new>
using namespace std;
ofstream out("Framis.out");
class Framis {
enum { sz = 10 };
char c[sz]; // occupiamo spazio, non utilizzato
static unsigned char pool[];
static bool mappa_allocazione[];
public:
enum { pdimensione = 100 }; // frami permesso
Framis() { out << "Framis()\n"; }
~Framis() { out << "~Framis() ... "; }
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*);
};
unsigned char Framis::pool[pdimensione * sizeof(Framis)];
bool Framis::mappa_allocazione[pdimensione] = {false};
// la dimensione è ignorata -- si assume un oggetto Framis
void*
Framis::operator new(size_t) throw(bad_alloc) {
for(int i = 0; i < pdimensione; i++)
if(!mappa_allocazione[i]) {
out << "utilizzo del blocco" << i << " ... ";
mappa_allocazione[i] = true; // memorizziamo che è usato
return pool + (i * sizeof(Framis));
}
out << "fine della memoria" << endl;
throw bad_alloc();
}
void Framis::operator delete(void* m) {
if(!m) return; // Controlliamo se è un puntatore null
// Si assume che sia stato creato nel pool
// calcolo del numero del blocco
unsigned long blocco = (unsigned long)m
- (unsigned long)pool;
blocco /= sizeof(Framis);
out << "rilascio blocco" << blocco<< endl;
// impostiamo libero
mappa_allocazione[blocco] = false;
}
int main() {
Framis* f[Framis::pdimensione];
try {
for(int i = 0; i < Framis::pdimensione; i++)
f[i] = new Framis;
new Framis; // Fine memoria
} catch(bad_alloc) {
cerr << "Fine memoria!" << endl;
}
delete f[10];
f[10] = 0;
// Uso della memoria liberata:
Framis* x = new Framis;
delete x;
for(int j = 0; j < Framis::pdimensione; j++)
delete f[j]; // Delete f[10] OK
} ///:~
Lo spazio di memoria per la heap di Framis viene creato allocando un array di byte grande abbastanza per gestire pdimensione oggetti Framis. La mappa di allocazione è lunga pdimensione elementi, quindi c'è un unico bool per ogni blocco. Tutti i valori della mappa di allocazione sono inizializzati a false, usando un trucco di inizializzazione che prevede di settare il primo elemento in modo che il compilatore automaticamente inizializza tutti gli altri al valore di default ( che è false, nel caso di bool).
L'operatore locale new() ha la stessa sintassi di quello globale. Tutto ciò che fa è cercare nella mappa di allocazione i valori false, poi setta quella locazione a true per indicare che è stato allocato e restituisce l'indirizzo del corrispondente blocco di memoria. Se non trova memoria, stampa un messaggio per tracciare il file e lancia un eccezione bad_alloc.
Questo è il primo esempio di eccezione che si vede in questo libro. Questo è un semplice utilizzo delle eccezioni, la cui discussione è rimandata al Volume 2. Nel operatore new() ci sono due punti importanti. Il primo, la lista degli argomenti della funzione è seguito da throw(bad_alloc), che dice al compilatore ed al lettore che questa funzione può lanciare un eccezione di tipo bad_alloc. Secondo, se non c'è più memoria la funzione lancia davvero l'eccezione con l'istruzione throw bad_alloc. Quando un eccezione viene lanciata, la funzione ferma l'esecuzione ed il controllo viene passato ad un gestore delle eccezione, che è espresso dalla clausola catch.
Nel main(), si vede l'altra parte della figura, che la clausola try-catch. Il blocco try è circondato da parentesi e contiene il codice che può lanciare le eccezioni, in questo caso qualsiasi chiamata a new che coinvolge oggetti Framis. Segue immediatamente al blocco try uno o più clausole catch, ognuna delle quali specifica il tipo di eccezione che trattano. In questo caso catch(bad_alloc) indica che le eccezioni bad_alloc saranno trattate qui. Questo particolare catch viene eseguito solo quando viene lanciata un'eccezione bad_alloc e l'esecuzione continua dopo la fine dell'ultima clausola catch del gruppo ( ce n'è una sola qui, ma ce ne potrebbero essere di più).
In questo esempio, va bene l'uso di iostreams perchè le versioni globali dell'operatore new() e delete() non sono state toccate.
L'operatore delete() assume che l'indirizzo di Framis sia stato creato nel pool. Ciò è corretto, perchè l'operatore locale new() verrà chiamato ogni volta si crea un singolo oggetto Framis nella heap, ma non un array di essi: il new globale viene usato per gli array. Quindi l'utente potrebbe aver usato accidentalmente l'operatore delete() senza la sintassi con le parentesi vuote per indicare la distruzione dell'array. Ciò causerebbe un problema. L'utente potrebbe anche cancellare un puntatore ad un oggetto creato nello stack. Se si pensa che queste cose accadono, sarebbe meglio aggiungere una linea per essere sicuri che l'indirizzo si trova nel pool e su un confine esatto ( si comincia a vedere anche il potenziale di new e delete sovraccaricati per trovare i memory leak).
L'operatore delete() calcola il blocco nel pool che questo puntatore rappresenta e poi imposta il flag della mappa di allocazione per quel blocco a false per indicare che il blocco è stato liberato.
In main(), vengono allocati tanti oggetti Framis quanto basta per finire la memoria, ciò evidenzia il comportamento in questo caso. Poi uno degli oggetti viene liberato ed un altro viene creato per mostrare che la memoria liberata viene riusata.
Poichè questo meccanismo di allocazione è specifico per gli oggetti Framis, probabilmente è molto più veloce di meccanismo di allocazione generico usato per default da new e delete. Tuttavia, si dovrebbe notare che non funziona automaticamete se viene utilizzata l'ereditarietà ( discussa nel Capitolo 14).
Se si vuole usare l'overload di new e delete per una classe, questi operatori vengono chiamati ogni volta che si crea un oggetto di una classe. Tuttavia, se si crea un array di questi oggetti delle classi, l'operatore globale new() viene chiamato per allocare abbastanza spazio per l'array tutto in una volta e l'operatore globale delete() viene chiamato per rilasciare quella memoria. Si può controllare l'allocazione di array di oggetti usando la versione speciale dell'overload per la classe. Ecco un esempio che mostra quando due diverse versioni vengono chiamate:
//: C13:ArrayOperatorNew.cpp
// Operatore new per arrays
#include <new> // Size_t definizione
#include <fstream>
using namespace std;
ofstream trace("ArrayOperatorNew.out");
class Widget {
enum { sz = 10 };
int i[sz];
public:
Widget() { trace << "*"; }
~Widget() { trace << "~"; }
void* operator new(size_t sz) {
trace << "Widget::new: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete(void* p) {
trace << "Widget::delete" << endl;
::delete []p;
}
void* operator new[](size_t sz) {
trace << "Widget::new[]: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete[](void* p) {
trace << "Widget::delete[]" << endl;
::delete []p;
}
};
int main() {
trace << "new Widget" << endl;
Widget* w = new Widget;
trace << "\ndelete Widget" << endl;
delete w;
trace << "\nnew Widget[25]" << endl;
Widget* wa = new Widget[25];
trace << "\ndelete []Widget" << endl;
delete []wa;
} ///:~
Qui, le versioni globali di new e delete vengono chiamate in modo che l'effetto è lo stesso di non avere le versioni con l'overload di new e delete tranne che viene aggiunta un informazioni di trace. Naturalmente, si può usare qualsiasi meccanismo di allocazione che si vuole con new e delete sovraccaricate.
Si può vedere che la sintassi di new e delete per gli array è la stessa per gli oggetti individuali tranne per l'aggiunta delle parentesi. In entrambi i casi si gestisce la dimensione della memoria che si deve allocare. La dimensione per versione array sarà la dimensione dell'intero array. Vale la pena di tenere in mente che l'unica cosa che è richiesta di fare con l'operatore new() è di fornire un puntatore ad un blocco di memoria abbastanza grande. Sebbene si può inizializzare la memoria, normalmente questo è il lavoro del costruttore che sarà chiamato automaticamente dal compilatore.
Il costruttore ed il distruttore stampano quindi si può vedere quando vengono chiamati. Ecco come appare il file di trace per un compilatore:
new Widget
Widget::new: 40 bytes
*
delete Widget
~Widget::delete
new Widget[25]
Widget::new[]: 1004 bytes
*************************
delete []Widget
~~~~~~~~~~~~~~~~~~~~~~~~~Widget::delete[]
Creare un oggetto individuale richiede 40 byte, come ci si aspetta. (Questa macchina usa quattro byte per un int). Viene chiamato prima l'operatore new() e poi il costruttore( indicato da un *). In maniera complementare, chiamando delete viene chiamato il prima il distruttore e poi l'operatore delete().
Quando viene creato un array di Widget, viene usata la versione array dell'operatore new(), come promesso. Ma si noti che la dimensione richiesta è più grande dei quattro byte che ci si aspetta. Questi ulteriori quattro byte servono al sistema per le informazioni sull'array, in particolare, il numero di oggetti dell'array. Quindi quando si scrive:
delete []Widget;
le parentesi dicono al compilatore che è un array di oggetti, quindi il compilatore genera codice per conoscere il numero di oggetti nell'array e per chiamare il distruttore tante volte quanto serve. Come si vede, sebbene l'operatore new() e delete() per array sono chiamati solo una volta per l'intero blocco di array, il costruttore ed il distruttore di default vengono chiamati per ogni oggetto dell'array.
Si consideri
MioTipo* f = new MioTipo;
chiama new per allocare un pezzo di memoria grande quanto MioTipo, poi invoca il costruttore di MioTipo sulla memoria, cosa accade se non si riesce ad allocare memoria con new? Il costruttore non viene chiamato in questo caso, quindi sebbene si crea un oggetto senza successo, almeno non si invoca il costruttore e si gestisce un puntatore this che vale zero. Ecco un esempio:
//: C13:NoMemory.cpp
// Il Costruttore non viene chiamato se new fallisce
#include <iostream>
#include <new> // definizione bad_alloc
using namespace std;
class NoMemory {
public:
FineMemoria() {
cout << "FineMemoria::FineMemoria()" << endl;
}
void* operator new(size_t sz) throw(bad_alloc){
cout << "FineMemoria::operatore new" << endl;
throw bad_alloc(); // "Fine Memoria"
}
};
int main() {
FineMemoria* nm = 0;
try {
nm = new FineMemoria;
} catch(bad_alloc) {
cerr << "eccezione: Memoria terminata" << endl;
}
cout << "nm = " << nm << endl;
} ///:~
Quando il programma viene eseguito, non stampa il messaggio del costruttore, ma il messaggio del operatore new() ed il messaggio del gestore dell'eccezione. Poichè new non ritorna mai, il costruttore non viene mai chiamato quindi il suo messaggio non viene stampato.
È importante che nm sia inizializzato a zero poichè l'espressione new non termina mai ed il puntatore dovrebbe essere zero per essere sicuri che non se ne faccia un cattivo uso. Tuttavia, si dovrebbe fare qualcosa in più nel gestore dell'eccezione che stampare solo un messaggio e continuare come se l'oggetto fosse stato creato correttamente. Idealmente si farà qualcosa che risolverà il problema e si uscirà dopo aver riportato l'errore su un file di log.
Nelle prime versioni del C++ era una pratica normale restituire zero da un new se falliva l'allocazione della memoria. Tuttavia, se si prova a restituire zero da un new con un compilatore Standard, ci viene detto che si dovrebbe invece lanciare un'eccezione di bad_alloc.
Ci sono altri due, meno comuni, utilizzi del overload per l'operatore new().
Si può volere piazzare un oggetto in una specifica locazione di memoria. Ciò ha particolare importanza con i sistemi embedded hardware dove un oggetto può essere sinonimo di un particolare pezzo di hardware.
Si può volere scegliere fra diversi allocatori quando si usa new.
Entrambe le situazione sono gestite con lo stesso meccanismo: l'operatore new() sovraccaricato accetta più di un argomento. Come si è visto prima, il primo argomento è sempre la dimensione dell'oggetto, che è calcolato di nascosto e passato dal compilatore. Ma l'altro argomento può essere qualsiasi cosa si voglia, l'indirizzo dove si vuole che l'oggetto sia piazzato, un riferimento alla funzione di allocazione della memoria oppure oggetto o qualsiasi altra cosa.
Il modo in cui si passa l'argomento extra all'operatore new() durante una chiamata può sembrare un pò curioso all'inizio. Si mette la lista degli argomenti ( senza l'argomento size_t, che è gestito dal compilatore) dopo la parola chiave new e prima del nome della classe dell'oggetto che si sta creando. Per esempio,
X* xp = new(a) X;
passerà a come secondo argomento all'operatore new(). Naturalmente, ciò funziona solo se tale operatore new() è stato dichiarato.
Ecco un esempio che mostra come su può piazzare un oggetto in una particolare locazione:
//: C13:PlacementOperatorNew.cpp
// Piazzamento con operatore new()
#include <cstddef> // Size_t
#include <iostream>
using namespace std;
class X {
int i;
public:
X(int ii = 0) : i(ii) {
cout << "this = " << this << endl;
}
~X() {
cout << "X::~X(): " << this << endl;
}
void* operator new(size_t, void* loc) {
return loc;
}
};
int main() {
int l[10];
cout << "l = " << l << endl;
X* xp = new(l) X(47); // X alla locazione location l
xp->X::~X(); // Chiamata esplicita al distruttore
// Da usare solo con il piazzamento!
} ///:~
Si noti che l'operatore new restituisce solo il puntatore che gli viene passato. Quindi, il chiamante decide dove l'oggetto verrà situato ed il costruttore viene chiamato per quella memoria come parte dell'espressione new.
Sebbene questo esempio mostra solo un argomento addizionale, nulla ci impedisce di aggiungerne altri se ce ne è il bisogno per altri scopi.
Un dilemma accade quando si vuole distruggere l'oggetto. C'è un'unica versione dell'operatore delete, quindi non c'è modo di dire: "Usa il mio speciale deallocatore per questo oggetto". Si vuole chiamare il distruttore, ma non si vuole che la memoria venga liberata dal meccanismo che gestisce la memoria dinamica poichè non è stata allocata nella heap.
La risposta è data da una sintassi molto speciale. Si può esplicitare la chiamata del distruttore:
xp->X::~X(); // Chiamata esplicita al distruttore
Un austero warning è di regola qui. Qualcuno vede ciò come un modo per distruggere gli oggetti un pò di tempo prima della fine dello scope, piuttosto che modificare lo scope o ( più correttamente ) usare la creazione dinamica degli oggetti se si vuole che il tempo di vita dell'oggetto sia determinato a runtime. Si avranno seri problemi se si chiama il distruttore in questo modo per un oggetto creato in modo normale sullo stack perchè il distruttore verrà chiamato di nuovo alla fine dello scope. Se si chiama il distruttore per un oggetto creato nella heap, il distruttore verrà eseguito, ma la memoria non verrà rilasciata, che probabilmente non è ciò che si vuole. L'unica ragione per cui il distruttore può essere chiamato esplicitamente in questo modo è supportare la sintassi per il piazzamento dell'operatore new.
C'è anche un operatore delete con piazzamento che viene chiamato solo se un costruttore per un new con piazzamento lanci un' eccezione ( in modo che la memoria viene automaticamente pulita durante l'eccezione). L'operatore delete con piazzamento ha una lista di argomenti che corrisponde all'operatore new con piazzamento che viene chiamato prima che il costruttore lancia l'eccezione. Questo argomento verrà esplorato nel capitolo della gestione delle eccezioni nel Volume 2.
È conveniente e molto efficiente creare oggetti automatici nello stack, ma per risolvere il programma della programmazione generale si deve poter creare e distruggere oggetti in ogni momento durante l'esecuzione di un programma, in particolare per rispondere alle informazioni dall'esterno del programma. Sebbene l'allocazione dinamica della memoria in C avverrà nella heap, essa non fornisce la facilità di utilizzo e le garanzie del C++. Portando la creazione dinamica di oggetti nel nucleo del linguaggio con new e delete, si possono creare oggetti nella heap facilmente come si fa nello stack. In aggiunta, si ottiene una grande flessibilità. Si può cambiare il comportamento di new e delete se non sono adatti ai propri bisogni, in particolare se non sono abbastanza efficienti. Si può anche modificare ciò che avviene quando finisce lo spazio nella heap.
Le soluzioni agli esercizi selezionati possono essere trovate nel documento elettronico The Thinking in C++ Annotated Solution Guide, disponibile in cambio di un piccolo onorario su www.BruceEckel.com.
[50] C'è un sintassi speciale chiamata placement new la quale permette di chiamare un costruttore per un pezzo di memoria pre-allocato. Questo argomento verrà introdotto più in avanti nel capitolo.
[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
Ultima
modifica: 12/11/2002