C'è una differenza importante tra i due.
Tutto ciò che non è allocato new
si comporta in modo simile ai tipi di valore in C # (e le persone spesso dicono che quegli oggetti sono allocati nello stack, che è probabilmente il caso più comune / ovvio, ma non sempre vero. Più precisamente, gli oggetti allocati senza usare new
hanno l'archiviazione automatica durata
Tutto l'allocato con new
viene allocato sull'heap e viene restituito un puntatore ad esso, esattamente come i tipi di riferimento in C #.
Qualunque cosa allocata nello stack deve avere una dimensione costante, determinata in fase di compilazione (il compilatore deve impostare correttamente il puntatore dello stack o se l'oggetto è un membro di un'altra classe, deve regolare la dimensione di quell'altra classe) . Ecco perché le matrici in C # sono tipi di riferimento. Devono essere, perché con i tipi di riferimento, possiamo decidere in fase di runtime quanta memoria chiedere. E lo stesso vale qui. Solo le matrici con dimensione costante (una dimensione che può essere determinata in fase di compilazione) possono essere allocate con durata di memorizzazione automatica (in pila). Le matrici di dimensioni dinamiche devono essere allocate sull'heap, chiamandonew
.
(Ed è qui che si interrompe qualsiasi somiglianza con C #)
Ora, qualsiasi cosa allocata nello stack ha una durata di archiviazione "automatica" (puoi effettivamente dichiarare una variabile come auto
, ma questo è il valore predefinito se non viene specificato nessun altro tipo di archiviazione, quindi la parola chiave non è realmente utilizzata nella pratica, ma è qui che viene da)
La durata della memorizzazione automatica indica esattamente come suona, la durata della variabile viene gestita automaticamente. Al contrario, qualsiasi elemento allocato sull'heap deve essere eliminato manualmente dall'utente. Ecco un esempio:
void foo() {
bar b;
bar* b2 = new bar();
}
Questa funzione crea tre valori che vale la pena considerare:
Alla riga 1, dichiara una variabile b
di tipobar
nello stack (durata automatica).
Alla riga 2, dichiara un bar
puntatore b2
sullo stack (durata automatica) e chiama nuovo, allocando abar
oggetto sull'heap. (durata dinamica)
Quando la funzione ritorna, si verifica quanto segue: in primo luogo, non b2
rientra nell'ambito di applicazione (l'ordine di distruzione è sempre opposto all'ordine di costruzione). Ma b2
è solo un puntatore, quindi non succede nulla, la memoria che occupa viene semplicemente liberata. E, soprattutto, la memoria a cui punta (l' bar
istanza nell'heap) NON viene toccata. Viene liberato solo il puntatore, poiché solo il puntatore aveva una durata automatica. Secondo,b
va al di fuori dell'ambito, quindi poiché ha una durata automatica, viene chiamato il suo distruttore e la memoria viene liberata.
E l' bar
istanza sul mucchio? Probabilmente è ancora lì. Nessuno si è preso la briga di cancellarlo, quindi abbiamo fatto trapelare memoria.
Da questo esempio, possiamo vedere che qualsiasi cosa con durata automatica è garantita per far chiamare il suo distruttore quando esce dal campo di applicazione. Questo è utile Ma tutto ciò che è allocato sull'heap dura finché ne abbiamo bisogno e può essere dimensionato dinamicamente, come nel caso degli array. Anche questo è utile. Possiamo usarlo per gestire le nostre allocazioni di memoria. E se la classe Foo allocasse un po 'di memoria sull'heap nel suo costruttore e cancellasse quella memoria nel suo distruttore. Quindi potremmo ottenere il meglio da entrambi i mondi, allocazioni di memoria sicure che sono garantite per essere nuovamente liberate, ma senza i limiti di forzare tutto per essere nello stack.
E questo è praticamente esattamente come funziona la maggior parte del codice C ++. Guarda le librerie standardstd::vector
per esempio. In genere viene allocato nello stack, ma può essere ridimensionato e ridimensionato in modo dinamico. E lo fa allocando internamente la memoria sull'heap, se necessario. L'utente della classe non lo vede mai, quindi non c'è alcuna possibilità di perdere memoria o dimenticare di ripulire ciò che hai assegnato.
Questo principio si chiama RAII (Resource Acquisition is Initialization) e può essere esteso a qualsiasi risorsa che deve essere acquisita e rilasciata. (socket di rete, file, connessioni al database, blocchi di sincronizzazione). Tutti possono essere acquisiti nel costruttore e rilasciati nel distruttore, quindi sei sicuro che tutte le risorse acquisite verranno liberate di nuovo.
Come regola generale, non utilizzare mai new / delete direttamente dal codice di alto livello. Avvolgilo sempre in una classe in grado di gestire la memoria per te e che assicurerà che venga liberata di nuovo. (Sì, potrebbero esserci delle eccezioni a questa regola. In particolare, i puntatori intelligenti richiedono di chiamare new
direttamente e passare il puntatore al suo costruttore, che quindi prende il posto e garantisce che delete
venga chiamato correttamente. Ma questa è ancora una regola empirica molto importante )