Esistono due tecniche di allocazione della memoria ampiamente utilizzate: allocazione automatica e allocazione dinamica. Comunemente, esiste una corrispondente regione di memoria per ciascuno: lo stack e l'heap.
Pila
Lo stack alloca sempre la memoria in modo sequenziale. Può farlo perché richiede il rilascio della memoria nell'ordine inverso (First-In, Last-Out: FILO). Questa è la tecnica di allocazione della memoria per variabili locali in molti linguaggi di programmazione. È molto, molto veloce perché richiede una contabilità minima e il prossimo indirizzo da allocare è implicito.
In C ++, questo si chiama archiviazione automatica perché l'archiviazione viene rivendicata automaticamente alla fine dell'ambito. Non appena viene completata l'esecuzione del blocco di codice corrente (delimitato utilizzando {}
), la memoria per tutte le variabili in quel blocco viene raccolta automaticamente. Questo è anche il momento in cui vengono invocati i distruttori per ripulire le risorse.
Mucchio
L'heap consente una modalità di allocazione della memoria più flessibile. La contabilità è più complessa e l'allocazione è più lenta. Poiché non esiste un punto di rilascio implicito, è necessario rilasciare manualmente la memoria, utilizzando delete
o delete[]
( free
in C). Tuttavia, l'assenza di un punto di rilascio implicito è la chiave per la flessibilità dell'heap.
Ragioni per utilizzare l'allocazione dinamica
Anche se l'utilizzo dell'heap è più lento e potenzialmente causa perdite di memoria o frammentazione della memoria, esistono casi di utilizzo perfettamente validi per l'allocazione dinamica, poiché è meno limitato.
Due motivi principali per utilizzare l'allocazione dinamica:
Non sai di quanta memoria hai bisogno al momento della compilazione. Ad esempio, quando si legge un file di testo in una stringa, di solito non si conosce la dimensione del file, quindi non è possibile decidere la quantità di memoria da allocare fino a quando non si esegue il programma.
Si desidera allocare memoria che persisterà dopo aver lasciato il blocco corrente. Ad esempio, potresti voler scrivere una funzione string readfile(string path)
che restituisce il contenuto di un file. In questo caso, anche se lo stack può contenere l'intero contenuto del file, non è possibile tornare da una funzione e mantenere il blocco di memoria allocato.
Perché l'allocazione dinamica è spesso superflua
In C ++ c'è un costrutto pulito chiamato distruttore . Questo meccanismo consente di gestire le risorse allineando la durata della risorsa con la durata di una variabile. Questa tecnica si chiama RAII ed è il punto distintivo di C ++. "Avvolge" le risorse in oggetti. std::string
è un esempio perfetto. Questo frammento:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
alloca effettivamente una quantità variabile di memoria. L' std::string
oggetto alloca memoria usando l'heap e la rilascia nel suo distruttore. In questo caso, non era necessario gestire manualmente alcuna risorsa e ottenere comunque i vantaggi dell'allocazione dinamica della memoria.
In particolare, implica che in questo frammento:
int main ( int argc, char* argv[] )
{
std::string * program = new std::string(argv[0]); // Bad!
delete program;
}
c'è allocazione dinamica della memoria non necessaria. Il programma richiede una maggiore digitazione (!) E introduce il rischio di dimenticare di deallocare la memoria. Lo fa senza alcun beneficio apparente.
Perché dovresti usare l'archiviazione automatica il più spesso possibile
Fondamentalmente, l'ultimo paragrafo lo riassume. L'uso della memorizzazione automatica il più spesso possibile rende i tuoi programmi:
- più veloce da scrivere;
- più veloce quando eseguito;
- meno soggetto a perdite di memoria / risorse.
Punti bonus
Nella domanda di riferimento, ci sono ulteriori preoccupazioni. In particolare, la seguente classe:
class Line {
public:
Line();
~Line();
std::string* mString;
};
Line::Line() {
mString = new std::string("foo_bar");
}
Line::~Line() {
delete mString;
}
In realtà è molto più rischioso da usare rispetto al seguente:
class Line {
public:
Line();
std::string mString;
};
Line::Line() {
mString = "foo_bar";
// note: there is a cleaner way to write this.
}
Il motivo è che std::string
definisce correttamente un costruttore di copie. Considera il seguente programma:
int main ()
{
Line l1;
Line l2 = l1;
}
Utilizzando la versione originale, questo programma probabilmente si arresterà in modo anomalo, poiché utilizza delete
due volte sulla stessa stringa. Usando la versione modificata, ogni Line
istanza possiede la propria istanza di stringa , ognuna con la propria memoria ed entrambe verranno rilasciate alla fine del programma.
Altre note
L'uso estensivo di RAII è considerato una buona pratica in C ++ per tutte le ragioni sopra. Tuttavia, vi è un ulteriore vantaggio che non è immediatamente evidente. Fondamentalmente, è meglio della somma delle sue parti. L'intero meccanismo è composto . Ridimensiona.
Se usi la Line
classe come blocco predefinito:
class Table
{
Line borders[4];
};
Poi
int main ()
{
Table table;
}
alloca quattro std::string
istanze, quattro Line
istanze, Table
un'istanza e tutti i contenuti della stringa e tutto viene liberato automagicamente .