Quando dovrei usare la nuova parola chiave in C ++?


273

Sto usando C ++ da un po 'di tempo e mi sono chiesto della nuova parola chiave. Semplicemente, dovrei usarlo o no?

1) Con la nuova parola chiave ...

MyClass* myClass = new MyClass();
myClass->MyField = "Hello world!";

2) Senza la nuova parola chiave ...

MyClass myClass;
myClass.MyField = "Hello world!";

Dal punto di vista dell'implementazione, non sembrano così diversi (ma sono sicuro che lo sono) ... Tuttavia, il mio linguaggio principale è C #, e ovviamente il primo metodo è quello a cui sono abituato.

La difficoltà sembra essere che il metodo 1 è più difficile da usare con le classi C ++ standard.

Quale metodo dovrei usare?

Aggiornamento 1:

Di recente ho usato la nuova parola chiave per memoria heap (o archivio gratuito ) per un array di grandi dimensioni che stava andando fuori campo (ovvero restituito da una funzione). Dove prima stavo usando la pila, che ha causato la corruzione di metà degli elementi al di fuori dell'ambito, il passaggio all'utilizzo dell'heap mi ha assicurato che gli elementi erano intatti. Sìì!

Aggiornamento 2:

Un mio amico recentemente mi ha detto che esiste una semplice regola per usare la newparola chiave; ogni volta che digiti new, digita delete.

Foobar *foobar = new Foobar();
delete foobar; // TODO: Move this to the right place.

Questo aiuta a prevenire perdite di memoria, poiché è sempre necessario posizionare l'eliminazione da qualche parte (ad esempio quando si taglia e si incolla su un distruttore o in altro modo).


6
La risposta breve è, usa la versione breve quando puoi farla franca. :)
jalf

11
Una tecnica migliore che scrivere sempre una corrispondente eliminazione: utilizzare contenitori STL e puntatori intelligenti come std::vectore std::shared_ptr. Questi avvolgono le chiamate verso newe deleteper te, quindi hai ancora meno probabilità di perdere memoria. Chiediti, per esempio: ricordi sempre di mettere un corrispondente deleteovunque si potrebbe generare un'eccezione? Mettere in deletemano s è più difficile di quanto si pensi.
AshleysBrain,

@nbolton Re: AGGIORNAMENTO 1 - Una delle cose belle di C ++ è che ti permette di archiviare tipi definiti dall'utente nello stack, mentre i rifiuti raccolti come langs come C # ti costringono a memorizzare i dati sull'heap . L'archiviazione dei dati sui consumi mucchio più risorse di memorizzazione dei dati nello stack , quindi si dovrebbe preferire la pila al mucchio , tranne quando il vostro UDT richiede una grande quantità di memoria per archiviare i propri dati. (Ciò significa anche che gli oggetti vengono passati per valore per impostazione predefinita). Una soluzione migliore al tuo problema sarebbe passare l'array alla funzione per riferimento .
Charles Addis,

Risposte:


304

Metodo 1 (usando new)

  • Alloca memoria per l'oggetto nel negozio gratuito (questa è spesso la stessa cosa dell'heap )
  • Richiede di esplicitamente deletel'oggetto in seguito. (Se non lo elimini, potresti creare una perdita di memoria)
  • La memoria rimane allocata fino a quando non viene deleteeseguita. (ad esempio potresti returnusare un oggetto che hai creato usando new)
  • L'esempio nella domanda perderà memoria a meno che il puntatore non sia deleted; e dovrebbe sempre essere eliminato , indipendentemente dal percorso di controllo adottato o se vengono generate eccezioni.

Metodo 2 (non utilizzo new)

  • Alloca memoria per l'oggetto nello stack (dove vanno tutte le variabili locali) In genere c'è meno memoria disponibile per lo stack; se si allocano troppi oggetti, si rischia l'overflow dello stack.
  • Non ti servirà deletepiù tardi.
  • La memoria non viene più allocata quando esce dall'ambito. (cioè non dovresti returnpuntare a un oggetto nello stack)

Per quanto riguarda quale usare; scegli il metodo che funziona meglio per te, dati i vincoli di cui sopra.

Alcuni casi semplici:

  • Se non vuoi preoccuparti di chiamare delete (e il potenziale rischio di perdite di memoria ) non dovresti usarlo new.
  • Se desideri restituire un puntatore al tuo oggetto da una funzione, devi utilizzare new

4
Un pignolo: credo che il nuovo operatore alloca memoria da "free store", mentre malloc alloca da "heap". Questi non sono garantiti per essere la stessa cosa, anche se in pratica lo sono di solito. Vedi gotw.ca/gotw/009.htm .
Fred Larson,

4
Penso che la tua risposta potrebbe essere più chiara su quale utilizzare. (Il 99% delle volte, la scelta è semplice. Usa il metodo 2, su un oggetto wrapper che chiama nuovo / elimina in costruttore / distruttore)
jalf

4
@jalf: il metodo 2 è quello che non utilizza il nuovo: - / In ogni caso, ci sono molte volte in cui il codice sarà molto più semplice (ad es. gestione dei casi di errore) usando il metodo 2 (quello senza il nuovo)
Daniel LeCheminant,

Un altro pignolo ... Dovresti rendere più ovvio che il primo esempio di Nick perde memoria, mentre il suo secondo no, anche di fronte alle eccezioni.
Arafangion

4
@Fred, Arafangion: grazie per la tua comprensione; Ho incorporato i tuoi commenti nella risposta.
Daniel LeCheminant

118

C'è una differenza importante tra i due.

Tutto ciò che non è allocato newsi 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 newhanno l'archiviazione automatica durata Tutto l'allocato con newviene 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 bdi tipobar nello stack (durata automatica).

Alla riga 2, dichiara un barpuntatore b2sullo 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 b2rientra 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' baristanza 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' baristanza 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 newdirettamente e passare il puntatore al suo costruttore, che quindi prende il posto e garantisce che deletevenga chiamato correttamente. Ma questa è ancora una regola empirica molto importante )


2
"Tutto ciò che non è allocato con il nuovo viene messo nello stack" Non nei sistemi su cui ho lavorato ... di solito i dati globali (statici) intializzati (e non avviati) vengono collocati nei loro segmenti. Ad esempio, segmenti di linker .data, .bss, ecc .... Pedante, lo so ...
Dan

Certo, hai ragione. Non stavo davvero pensando ai dati statici. Mio male, ovviamente. :)
jalf

2
Perché qualcosa allocato nello stack deve avere una dimensione costante?
user541686,

Non sempre , ci sono alcuni modi per aggirarlo, ma in generale lo fa, perché è in pila. Se è nella parte superiore della pila, potrebbe essere possibile ridimensionarlo, ma una volta che qualcos'altro viene spinto sopra di esso, viene "murato", circondato da oggetti su entrambi i lati, quindi non può davvero essere ridimensionato . Sì, dicendo che sempre deve avere una dimensione fissa è un po 'di una semplificazione, ma veicola l'idea di base (e non lo consiglio pasticciare con le funzioni C che consentono di essere troppo creativo con allocazioni dello stack)
JALF

14

Quale metodo dovrei usare?

Questo non è quasi mai determinato dalle tue preferenze di digitazione ma dal contesto. Se è necessario mantenere l'oggetto su alcune pile o se è troppo pesante per la pila, allocarlo nell'archivio gratuito. Inoltre, poiché stai allocando un oggetto, sei anche responsabile del rilascio della memoria. Cerca l' deleteoperatore.

Per alleggerire l'onere dell'utilizzo della gestione del negozio libero, le persone hanno inventato cose come auto_ptre unique_ptr. Consiglio vivamente di dare un'occhiata a questi. Potrebbero anche essere di aiuto per i tuoi problemi di battitura ;-)


10

Se stai scrivendo in C ++ probabilmente stai scrivendo per le prestazioni. L'uso del negozio nuovo e gratuito è molto più lento rispetto all'uso dello stack (specialmente quando si usano i thread), quindi usarlo solo quando ne hai bisogno.

Come altri hanno già detto, hai bisogno di nuovi quando il tuo oggetto deve vivere al di fuori della funzione o dell'ambito dell'oggetto, l'oggetto è veramente grande o quando non conosci le dimensioni di un array al momento della compilazione.

Inoltre, cerca di evitare di usare mai delete. Avvolgi invece il tuo nuovo in un puntatore intelligente. Lascia che la chiamata del puntatore intelligente venga eliminata per te.

Ci sono alcuni casi in cui un puntatore intelligente non è intelligente. Non conservare mai std :: auto_ptr <> all'interno di un contenitore STL. Eliminerà il puntatore troppo presto a causa delle operazioni di copia all'interno del contenitore. Un altro caso è quando si dispone di un contenitore STL veramente grande di puntatori agli oggetti. boost :: shared_ptr <> avrà una tonnellata di sovraccarico di velocità mentre salta su e giù il conteggio dei riferimenti. Il modo migliore per andare in quel caso è quello di mettere il contenitore STL in un altro oggetto e dare a quell'oggetto un distruttore che chiamerà delete su ogni puntatore nel contenitore.


10

La risposta breve è: se sei un principiante in C ++, non dovresti mai usare newodelete da soli.

Invece, dovresti usare i puntatori intelligenti come std::unique_ptre std::make_unique(o meno spesso std::shared_ptre std::make_shared). In questo modo, non devi preoccuparti tanto delle perdite di memoria. E anche se sei più avanzato, la migliore pratica sarebbe di solito incapsulare il modo in cui stai utilizzando newe deletein una piccola classe (come un puntatore intelligente personalizzato) dedicata solo ai problemi del ciclo di vita degli oggetti.

Naturalmente, dietro le quinte, questi puntatori intelligenti stanno ancora eseguendo allocazione dinamica e deallocazione, quindi il codice che li utilizza avrebbe comunque il sovraccarico di runtime associato. Altre risposte qui hanno riguardato questi problemi e come prendere decisioni di progettazione su quando usare i puntatori intelligenti anziché semplicemente creare oggetti nello stack o incorporarli come membri diretti di un oggetto, abbastanza bene che non li ripeterò. Ma il mio riassunto esecutivo sarebbe: non usare i puntatori intelligenti o l'allocazione dinamica fino a quando qualcosa non ti costringe a farlo.


interessante vedere come una risposta può cambiare col passare del tempo;)
Wolf


2

La semplice risposta è sì: new () crea un oggetto nell'heap (con lo sfortunato effetto collaterale che devi gestire la sua durata (chiamando esplicitamente cancellalo su di esso), mentre il secondo modulo crea un oggetto nello stack nell'attuale ambito e quell'oggetto sarà distrutto quando esce dal campo di applicazione.


1

Se la tua variabile viene utilizzata solo nel contesto di una singola funzione, è meglio usare una variabile di stack, ovvero l'opzione 2. Come altri hanno già detto, non è necessario gestire la durata delle variabili di stack: sono costruite e distrutto automaticamente. Inoltre, l'allocazione / deallocazione di una variabile sull'heap è lenta al confronto. Se la tua funzione viene chiamata abbastanza spesso, vedrai un enorme miglioramento delle prestazioni se usi le variabili dello stack rispetto alle variabili dell'heap.

Detto questo, ci sono un paio di casi ovvi in ​​cui le variabili dello stack sono insufficienti.

Se la variabile dello stack ha un ingombro di memoria elevato, si corre il rischio di overflow dello stack. Per impostazione predefinita, la dimensione dello stack di ciascun thread è 1 MB su Windows. È improbabile che tu crei una variabile di stack di dimensioni pari a 1 MB, ma devi tenere presente che l'utilizzo dello stack è cumulativo. Se la tua funzione chiama una funzione che chiama un'altra funzione che chiama un'altra funzione che ..., le variabili dello stack in tutte queste funzioni occupano spazio sullo stesso stack. Le funzioni ricorsive possono incorrere rapidamente in questo problema, a seconda della profondità della ricorsione. Se questo è un problema, è possibile aumentare le dimensioni dello stack (non consigliato) o allocare la variabile sull'heap utilizzando il nuovo operatore (consigliato).

L'altra condizione più probabile è che la tua variabile abbia bisogno di "vivere" oltre lo scopo della tua funzione. In questo caso, assegnereste la variabile sull'heap in modo che possa essere raggiunta al di fuori dell'ambito di una determinata funzione.


1

Stai passando myClass da una funzione o ti aspetti che esista al di fuori di quella funzione? Come altri hanno detto, si tratta di ambito quando non si sta allocando sull'heap. Quando si lascia la funzione, questa scompare (eventualmente). Uno dei classici errori commessi dai principianti è il tentativo di creare un oggetto locale di una classe in una funzione e restituirlo senza allocarlo sull'heap. Ricordo il debug di questo genere di cose nei miei primi giorni facendo c ++.


0

Il secondo metodo crea l'istanza nello stack, insieme ad elementi dichiarati inte l'elenco dei parametri che vengono passati alla funzione.

Il primo metodo fa spazio a un puntatore nello stack, che è stato impostato nella posizione in memoria in cui un nuovo MyClassè stato allocato nell'heap o nel negozio libero.

Il primo metodo richiede anche che tu deletecrei ciò che crei new, mentre nel secondo metodo, la classe viene automaticamente distrutta e liberata quando non rientra nell'ambito (la parentesi graffa successiva, di solito).


-1

La risposta breve è sì, la "nuova" parola chiave è incredibilmente importante in quanto quando la si utilizza, i dati dell'oggetto vengono archiviati nell'heap anziché nello stack, che è molto importante!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.