Va notato che è, nel caso del C ++, un malinteso comune che "è necessario fare la gestione manuale della memoria". Di fatto, di solito non esegui alcuna gestione della memoria nel tuo codice.
Oggetti a dimensione fissa (con durata dell'ambito)
Nella maggior parte dei casi quando hai bisogno di un oggetto, l'oggetto avrà una durata definita nel tuo programma e verrà creato nello stack. Funziona per tutti i tipi di dati primitivi incorporati, ma anche per le istanze di classi e strutture:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Gli oggetti stack vengono rimossi automaticamente al termine della funzione. In Java, gli oggetti vengono sempre creati nell'heap e pertanto devono essere rimossi da un meccanismo come la garbage collection. Questo non è un problema per gli oggetti stack.
Oggetti che gestiscono i dati dinamici (con durata dell'ambito)
L'uso dello spazio sulla pila funziona per oggetti di dimensioni fisse. Quando hai bisogno di una quantità variabile di spazio, come un array, viene utilizzato un altro approccio: la lista è incapsulata in un oggetto a dimensione fissa che gestisce la memoria dinamica per te. 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 dal modo in cui lasciamo l'ambito di listTest
, sia tramite un'eccezione o semplicemente ritornando da esso, verrà chiamato il distruttore ~MyList()
e non è necessario gestire alcuna memoria.
(Penso che sia una decisione divertente progettare di usare l'operatore 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 necessitano di memoria dinamica usano questo incapsulamento. È stato chiamato RAII ("acquisizione delle risorse è inizializzazione"), che è piuttosto un modo strano per esprimere la semplice idea che gli oggetti si preoccupino dei propri contenuti; quello che acquistano è il loro da pulire.
Oggetti polimorfici e durata oltre l'ambito
Ora, entrambi questi casi erano per la memoria che ha una durata ben definita: la durata è la stessa dell'oscilloscopio. 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 anche utilizzati quando si hanno istanze di oggetti il cui tipo varia in fase di esecuzione, ma che hanno un'interfaccia o una classe base comune:
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.
}
C'è un altro tipo di puntatore intelligente, std::shared_ptr
, per condividere oggetti tra diversi client. Eliminano solo il loro oggetto contenuto 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 breve, vediamo che non esegui alcuna gestione manuale della memoria. Tutto è incapsulato e viene poi gestito con una gestione della memoria completamente automatica e basata su un ambito. Nei casi in cui questo non è sufficiente, vengono utilizzati puntatori intelligenti che incapsulano la memoria non elaborata.
È considerata una pratica estremamente scorretta utilizzare i puntatori grezzi come proprietari di risorse ovunque nel codice C ++, allocazioni non elaborate al di fuori dei costruttori e chiamate delete
al di fuori dei distruttori, poiché sono quasi impossibili da gestire quando si verificano eccezioni e in generale difficile da usare in sicurezza.
La migliore: funziona per 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 (locking / unlocking). Fondamentalmente, ogni risorsa che può essere acquisita e deve essere rilasciata viene gestita esattamente nello stesso modo in C ++ e nessuna di queste viene lasciata all'utente. È tutto incapsulato in classi che acquisiscono nel costruttore e rilasciano nel distruttore.
Ad esempio, una funzione che blocca un mutex è 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, richiedendo che tu lo faccia manualmente (ad esempio in una clausola finally
) o generano meccanismi specializzati che risolvono questo problema, ma non in un modo particolarmente elegante (di solito più tardi nella loro vita, quando abbastanza persone hanno sofferto della mancanza). Tali meccanismi sono try-with-resources in Java e l'istruzione using in C #, entrambi sono approssimazioni del 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 memoria e persino la gestione delle risorse in C ++ non sono di solito "manuali", ma in realtà per lo più automatico.