Va notato che è, nel caso del C ++, un malinteso comune che "è necessario eseguire la gestione manuale della memoria". In effetti, di solito non si esegue alcuna gestione della memoria nel codice.
Oggetti di dimensioni fisse (con durata dell'ambito)
Nella stragrande maggioranza dei casi, quando è necessario un oggetto, l'oggetto avrà una durata definita nel programma e verrà creato nello stack. Funziona con tutti i tipi di dati primitivi integrati, ma anche con istanze di classi e strutture:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Gli oggetti dello stack vengono rimossi automaticamente al termine della funzione. In Java, gli oggetti vengono sempre creati sull'heap e quindi devono essere rimossi da alcuni meccanismi come la garbage collection. Questo non è un problema per gli oggetti dello stack.
Oggetti che gestiscono dati dinamici (con durata dell'ambito)
L'uso dello spazio nello stack funziona per oggetti di dimensioni fisse. Quando è necessaria una quantità variabile di spazio, ad esempio un array, viene utilizzato un altro approccio: l'elenco viene incapsulato in un oggetto di dimensioni fisse che gestisce la memoria dinamica per l'utente. Questo funziona perché gli oggetti possono avere una funzione di pulizia speciale, il distruttore. È garantito che venga chiamato quando l'oggetto esce dall'ambito e fa l'opposto del costruttore:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
Non esiste alcuna gestione della memoria nel codice in cui viene utilizzata la memoria. L'unica cosa che dobbiamo accertarci è che l'oggetto che abbiamo scritto abbia un distruttore adatto. Indipendentemente da come lasciamo il campo di applicazione listTest
, che si tratti di un'eccezione o semplicemente di ritorno da esso, il distruttore ~MyList()
verrà chiamato e non è necessario gestire alcuna memoria.
(Penso che sia una decisione di progettazione divertente usare l' operatore binario NOT~
, per indicare il distruttore. Quando usato sui numeri, inverte i bit; in analogia, qui indica che ciò che il costruttore ha fatto è invertito.)
Fondamentalmente tutti gli oggetti C ++ che richiedono memoria dinamica usano questo incapsulamento. È stato chiamato RAII ("acquisizione di risorse è inizializzazione"), che è un modo abbastanza strano di esprimere la semplice idea che gli oggetti si preoccupino del proprio contenuto; quello che acquisiscono è il loro da ripulire.
Oggetti polimorfici e durata oltre lo scopo
Ora, entrambi questi casi riguardavano una memoria che ha una durata chiaramente definita: la durata è la stessa dell'ambito. Se non vogliamo che un oggetto scada quando lasciamo l'ambito, esiste un terzo meccanismo che può gestire la memoria per noi: un puntatore intelligente. I puntatori intelligenti vengono utilizzati anche quando si hanno istanze di oggetti il cui tipo varia in fase di esecuzione, ma che hanno un'interfaccia o una classe base comuni:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
Esiste un altro tipo di puntatore intelligente std::shared_ptr
, per la condivisione di oggetti tra più client. Eliminano il loro oggetto contenuto solo quando l'ultimo client esce dall'ambito, quindi possono essere utilizzati in situazioni in cui è completamente sconosciuto quanti client ci saranno e per quanto tempo useranno l'oggetto.
In sintesi, vediamo che in realtà non si esegue alcuna gestione manuale della memoria. Tutto è incapsulato e quindi curato attraverso una gestione della memoria completamente automatica e basata sull'ambito. Nei casi in cui ciò non è sufficiente, vengono utilizzati i puntatori intelligenti che incapsulano la memoria non elaborata.
È considerata una cattiva pratica utilizzare puntatori non elaborati come proprietari di risorse in qualsiasi punto del codice C ++, allocazioni non elaborate all'esterno dei costruttori e delete
chiamate non elaborate all'esterno dei distruttori, poiché sono quasi impossibili da gestire quando si verificano eccezioni e generalmente difficili da utilizzare in modo sicuro.
Il meglio: funziona con tutti i tipi di risorse
Uno dei maggiori vantaggi di RAII è che non si limita alla memoria. In realtà fornisce un modo molto naturale per gestire risorse come file e socket (apertura / chiusura) e meccanismi di sincronizzazione come mutex (blocco / sblocco). Fondamentalmente, ogni risorsa che può essere acquisita e deve essere rilasciata è gestita esattamente allo stesso modo in C ++ e nessuna di questa gestione è lasciata all'utente. È tutto incapsulato in classi che acquisiscono nel costruttore e rilasciano nel distruttore.
Ad esempio, una funzione che blocca un mutex viene solitamente scritta in questo modo in C ++:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
Altre lingue lo rendono molto più complicato, richiedendoti di farlo manualmente (ad esempio in una finally
clausola) o generando meccanismi specializzati che risolvono questo problema, ma non in un modo particolarmente elegante (di solito più tardi nella loro vita, quando abbastanza persone hanno ha sofferto per la mancanza). Tali meccanismi sono try-with-resources in Java e l' istruzione using in C #, entrambi i quali sono approssimazioni della RAII di C ++.
Quindi, per riassumere, tutto questo era un resoconto molto superficiale di RAII in C ++, ma spero che aiuti i lettori a capire che la gestione della memoria e persino delle risorse in C ++ non è solitamente "manuale", ma in realtà per lo più automatica.