Perché i programmatori C ++ dovrebbero ridurre al minimo l'uso di "nuovo"?


873

Mi sono imbattuto nella domanda di overflow dello stack Perdita di memoria con std :: string quando si utilizza std :: list <std :: string> e uno dei commenti dice questo:

Smetti di usare newcosì tanto. Non riesco a vedere alcun motivo per cui hai usato nuovo ovunque tu abbia fatto. Puoi creare oggetti in base al valore in C ++ ed è uno degli enormi vantaggi dell'uso del linguaggio.
Non è necessario allocare tutto sull'heap.
Smetti di pensare come un programmatore Java .

Non sono davvero sicuro di cosa voglia dire.

Perché gli oggetti dovrebbero essere creati in base al valore in C ++ il più spesso possibile e che differenza fa internamente?
Ho interpretato male la risposta?

Risposte:


1037

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 deleteo delete[]( freein 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::stringoggetto 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::stringdefinisce 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 deletedue volte sulla stessa stringa. Usando la versione modificata, ogni Lineistanza 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 Lineclasse come blocco predefinito:

 class Table
 {
      Line borders[4];
 };

Poi

 int main ()
 {
     Table table;
 }

alloca quattro std::stringistanze, quattro Lineistanze, Tableun'istanza e tutti i contenuti della stringa e tutto viene liberato automagicamente .


55
+1 per menzionare RAII alla fine, ma dovrebbe esserci qualcosa sulle eccezioni e sullo svolgersi dello stack.
Tobu,

7
@Tobu: sì, ma questo post è già piuttosto lungo, e volevo tenerlo piuttosto concentrato sulla domanda di OP. Finirò per scrivere un post sul blog o qualcosa del genere e collegarlo ad esso da qui.
André Caron,

15
Sarebbe un ottimo complemento per menzionare il rovescio della medaglia per l'allocazione dello stack (almeno fino a C ++ 1x): spesso non è necessario copiare le cose inutilmente. ad es. a Monstersputa Treasurea al Worldquando muore. Nel suo Die()metodo aggiunge il tesoro al mondo. Deve usarlo world->Add(new Treasure(/*...*/))in altri per preservare il tesoro dopo la sua morte. Le alternative sono shared_ptr(può essere eccessivo), auto_ptr(scarsa semantica per il trasferimento di proprietà), passaggio per valore (spreco) e move+ unique_ptr(non ancora ampiamente implementato).
kizzx2,

7
Quello che hai detto sulle variabili locali allocate nello stack potrebbe essere un po 'fuorviante. "Lo stack" si riferisce allo stack di chiamate, che memorizza i frame dello stack . Sono questi telai stack che sono memorizzati in modo LIFO. Le variabili locali per un frame specifico sono allocate come se fossero membri di una struttura.
Someguy,

7
@someguy: In effetti, la spiegazione non è perfetta. L'implementazione ha libertà nella sua politica di assegnazione. Tuttavia, le variabili devono essere inizializzate e distrutte in modo LIFO, quindi vale l'analogia. Non penso che sia complicato ulteriormente la risposta.
André Caron,

171

Perché lo stack è più veloce e a prova di perdite

In C ++, ci vuole solo una singola istruzione per allocare spazio - nello stack - per ogni oggetto scope locale in una data funzione, ed è impossibile perdere parte di quella memoria. Quel commento intendeva (o avrebbe dovuto essere inteso) dire qualcosa del tipo "usa lo stack e non l'heap".


18
"ci vuole solo una singola istruzione per allocare spazio" - oh, sciocchezze. Sicuramente ci vuole solo un'istruzione da aggiungere al puntatore dello stack, ma se la classe ha una struttura interna interessante ci sarà molto di più che aggiungere al puntatore dello stack in corso. È ugualmente valido affermare che in Java non servono istruzioni per allocare spazio, poiché il compilatore gestirà i riferimenti al momento della compilazione.
Charlie Martin,

31
@Charlie è corretto. Le variabili automatiche sono veloci e infallibili sarebbe più preciso.
Oliver Charlesworth,

28
@Charlie: gli interni della classe devono essere impostati in entrambi i modi. Il confronto è in corso sull'allocazione dello spazio richiesto.
Oliver Charlesworth,

51
tosse int x; return &x;
Peter

16
veloce sì. Ma certamente non infallibile. Niente è infallibile. È possibile ottenere uno StackOverflow :)
rxantos,

107

Il motivo per cui è complicato.

Innanzitutto, il C ++ non viene raccolto. Pertanto, per ogni nuovo, deve esserci un'eliminazione corrispondente. Se non riesci a inserire questa eliminazione, allora hai una perdita di memoria. Ora, per un caso semplice come questo:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

Questo è semplice Ma cosa succede se "Do stuff" genera un'eccezione? Spiacenti: perdita di memoria. Cosa succede se "Fai cose" si presenta returnpresto? Spiacenti: perdita di memoria.

E questo è per il caso più semplice . Se ti capita di restituire quella stringa a qualcuno, ora deve eliminarla. E se lo passano come argomento, la persona che lo riceve deve eliminarlo? Quando dovrebbero eliminarlo?

Oppure puoi semplicemente fare questo:

std::string someString(...);
//Do stuff

No delete. L'oggetto è stato creato sullo "stack" e verrà distrutto una volta uscito dal campo di applicazione. È anche possibile restituire l'oggetto, trasferendo così il suo contenuto nella funzione chiamante. È possibile passare l'oggetto alle funzioni (in genere come riferimento o const-reference:. void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)E così via.

Tutto senza newe delete. Non c'è dubbio su chi sia il proprietario della memoria o chi sia responsabile della sua eliminazione. Se fate:

std::string someString(...);
std::string otherString;
otherString = someString;

Resta inteso che otherStringha una copia dei dati di someString. Non è un puntatore; è un oggetto separato. Può capitare che abbiano gli stessi contenuti, ma puoi cambiarne uno senza influenzare l'altro:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Vedi l'idea?


1
A tale proposito ... Se un oggetto viene allocato in modo dinamico main(), esiste per la durata del programma, non può essere facilmente creato nello stack a causa della situazione e i puntatori ad esso vengono passati a tutte le funzioni che richiedono l'accesso ad esso , può causare una perdita in caso di arresto anomalo del programma o sarebbe sicuro? Vorrei assumere quest'ultimo, dal momento che il sistema operativo che sta deallocando tutta la memoria del programma dovrebbe logicamente anche allocarlo, ma non voglio assumere nulla quando si tratta di new.
Justin Time - Ripristina Monica il

4
@JustinTime Non devi preoccuparti di liberare memoria dagli oggetti allocati dinamicamente che devono rimanere per la durata del programma. Quando viene eseguito un programma, il sistema operativo crea un atlante di memoria fisica, o memoria virtuale, per esso. Ogni indirizzo nello spazio di memoria virtuale viene mappato su un indirizzo di memoria fisica e quando il programma viene chiuso, viene liberato tutto ciò che è mappato sulla sua memoria virtuale. Quindi, finché il programma termina completamente, non devi preoccuparti che la memoria allocata non venga mai cancellata.
Aiman ​​Al-Eryani,

75

Gli oggetti creati da newdevono essere eventualmente deletepersi. Il distruttore non verrà chiamato, la memoria non verrà liberata, l'intero bit. Poiché C ++ non ha Garbage Collection, è un problema.

Gli oggetti creati in base al valore (ovvero in pila) muoiono automaticamente quando escono dall'ambito. La chiamata distruttore viene inserita dal compilatore e la memoria viene liberata automaticamente al ritorno della funzione.

I puntatori intelligenti come unique_ptr, shared_ptrrisolvono il problema di riferimento penzolante, ma richiedono una disciplina di codifica e hanno altri potenziali problemi (copiabilità, loop di riferimento, ecc.).

Inoltre, in scenari fortemente multithread, newc'è un punto di contesa tra i thread; ci può essere un impatto sulle prestazioni per un uso eccessivo new. La creazione di oggetti stack è per definizione thread-local, poiché ogni thread ha il proprio stack.

Il rovescio della medaglia degli oggetti valore è che muoiono una volta che la funzione host ritorna - non è possibile passare un riferimento a quelli indietro al chiamante, solo copiando, restituendo o spostando per valore.


9
+1. Ri "Gli oggetti creati da newdevono eventualmente finire deleteper non perdere." - peggio ancora, new[]deve essere abbinato delete[]e si ottiene un comportamento indefinito se si utilizza la delete new[]memoria o la delete[] newmemoria - pochissimi compilatori lo avvertono (alcuni strumenti come Cppcheck fanno quando possono).
Tony Delroy,

3
@TonyDelroy Ci sono situazioni in cui il compilatore non può avvisarlo. Se una funzione restituisce un puntatore, potrebbe essere creata se nuova (un singolo elemento) o nuova [].
fbafelipe,

32
  • C ++ non impiega alcun gestore di memoria da solo. Altre lingue come C #, Java ha Garbage Collector per gestire la memoria
  • Le implementazioni C ++ in genere utilizzano le routine del sistema operativo per allocare la memoria e troppe novità / eliminazioni potrebbero frammentare la memoria disponibile
  • Con qualsiasi applicazione, se la memoria viene utilizzata frequentemente, è consigliabile pre-allocarla e rilasciarla quando non è necessaria.
  • Una gestione impropria della memoria potrebbe causare perdite di memoria ed è davvero difficile da rintracciare. Quindi l'uso di oggetti stack nell'ambito della funzione è una tecnica collaudata
  • L'aspetto negativo dell'utilizzo di oggetti stack è che crea più copie di oggetti al ritorno, passando a funzioni ecc. Tuttavia i compilatori intelligenti sono ben consapevoli di queste situazioni e sono stati ottimizzati bene per le prestazioni
  • È davvero noioso in C ++ se la memoria viene allocata e rilasciata in due luoghi diversi. La responsabilità del rilascio è sempre una domanda e per lo più ci affidiamo ad alcuni puntatori comunemente accessibili, oggetti stack (massimo possibile) e tecniche come auto_ptr (oggetti RAII)
  • La cosa migliore è che hai il controllo della memoria e la cosa peggiore è che non avrai alcun controllo sulla memoria se utilizziamo una gestione della memoria impropria per l'applicazione. Gli arresti anomali causati dalla corruzione della memoria sono i più cattivi e difficili da rintracciare.

5
In realtà, qualsiasi lingua che alloca memoria ha un gestore di memoria, incluso c. Molti sono semplicemente molto semplici, cioè int * x = malloc (4); int * y = malloc (4); ... la prima chiamata alloca la memoria, ovvero chiede la memoria al sistema operativo (di solito in blocchi 1k / 4k) in modo che la seconda chiamata, non alloca effettivamente la memoria, ma ti dia un pezzo dell'ultimo pezzo per cui è stato allocato. IMO, i garbage collector non sono gestori di memoria, perché gestiscono solo la deallocazione automatica della memoria. Per essere chiamato un gestore di memoria, non dovrebbe solo gestire la deallocazione ma anche l'allocazione della memoria.
Rahly,

1
Le variabili locali usano lo stack in modo che il compilatore non emetta chiamate malloc()o i suoi amici per allocare la memoria richiesta. Tuttavia, lo stack non può rilasciare alcun elemento all'interno dello stack, l'unico modo in cui la memoria dello stack viene mai rilasciata è svolgersi dalla parte superiore dello stack.
Mikko Rantalainen,

C ++ non "usa le routine del sistema operativo"; non fa parte del linguaggio, è solo un'implementazione comune. Il C ++ potrebbe anche essere in esecuzione senza alcun sistema operativo.
einpoklum,

22

Vedo che mancano alcuni motivi importanti per fare il minor numero possibile di nuovi:

L'operatore newha un tempo di esecuzione non deterministico

chiamata new può o meno indurre il sistema operativo ad allocare una nuova pagina fisica al tuo processo, ciò può essere piuttosto lento se lo fai spesso. O potrebbe già avere una posizione di memoria adatta pronta, non lo sappiamo. Se il tuo programma deve avere tempi di esecuzione coerenti e prevedibili (come in un sistema in tempo reale o una simulazione di gioco / fisica), devi evitare newnei tuoi cicli temporali critici.

Operatore new è una sincronizzazione implicita del thread

Sì, mi hai sentito, il tuo sistema operativo deve assicurarsi che le tabelle delle tue pagine siano coerenti e, in quanto tale chiamata new, il thread acquisirà un blocco mutex implicito. Se stai chiamando costantementenew da molti thread, stai effettivamente serializzando i tuoi thread (l'ho fatto con 32 CPU, ognuna delle quali ha funzionato newper ottenere alcune centinaia di byte ciascuna, ahi! Quella era una pita reale per il debug)

Il resto come lento, frammentazione, soggetto a errori, ecc. Sono già stati citati da altre risposte.


2
Entrambi possono essere evitati utilizzando il posizionamento new / delete e allocando la memoria in anticipo. Oppure puoi allocare / liberare la memoria tu stesso e quindi chiamare il costruttore / distruttore. Questo è il modo in cui di solito funziona std :: vector.
rxantos,

1
@rxantos Leggere OP, questa domanda è sull'evitare allocazioni di memoria non necessarie. Inoltre, non esiste l'eliminazione del posizionamento.
Emily L.,

@Emily Questo è ciò che significava l'OP, penso:void * someAddress = ...; delete (T*)someAddress
xryl669,

1
L'uso dello stack non è deterministico neanche nel tempo di esecuzione. A meno che tu non abbia chiamato mlock()o qualcosa di simile. Questo perché il sistema potrebbe esaurire la memoria e non ci sono pagine di memoria fisica pronte disponibili per lo stack, quindi il sistema operativo potrebbe dover scambiare o scrivere alcune cache (svuota la memoria sporca) sul disco prima che l'esecuzione possa procedere.
Mikko Rantalainen,

1
@mikkorantalainen è tecnicamente vero, ma in una situazione di memoria insufficiente tutte le scommesse sono in ogni caso fuori prestazione mentre stai spingendo su disco, quindi non c'è nulla che tu possa fare. In ogni caso non invalida il consiglio di evitare nuove chiamate quando è ragionevole farlo.
Emily L.,

21

Pre-C ++ 17:

Perché è soggetto a perdite sottili anche se si avvolge il risultato in un puntatore intelligente .

Considera un utente "attento" che ricorda di avvolgere gli oggetti nei puntatori intelligenti:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Questo codice è pericoloso perché non v'è alcuna garanzia che o shared_ptrè costruito prima di uno T1o T2. Quindi, se uno degli new T1()o new T2()fallisce dopo che l'altro ha successo, allora il primo oggetto sarà trapelato perché non shared_ptresiste per distruggerlo e deallocare.

Soluzione: utilizzare make_shared .

Post-C ++ 17:

Questo non è più un problema: C ++ 17 impone un vincolo all'ordine di queste operazioni, garantendo in questo caso che ogni chiamata a new()deve essere immediatamente seguita dalla costruzione del corrispondente puntatore intelligente, senza altre operazioni intermedie. Ciò implica che, al momento in cui new()viene chiamato il secondo , è garantito che il primo oggetto è già stato racchiuso nel suo puntatore intelligente, evitando così eventuali perdite nel caso in cui venga generata un'eccezione.

Una spiegazione più dettagliata del nuovo ordine di valutazione introdotto da C ++ 17 è stata fornita da Barry in un'altra risposta .

Grazie a @Remy Lebeau per aver sottolineato che questo è ancora un problema in C ++ 17 (anche se meno): il shared_ptrcostruttore può non riuscire ad allocare il suo blocco di controllo e lanciare, nel qual caso il puntatore passato ad esso non viene cancellato.

Soluzione: utilizzaremake_shared .


5
Altra soluzione: non allocare mai dinamicamente più di un oggetto per riga.
Antimonio

3
@Antimonio: Sì, è molto più allettante assegnare più di un oggetto quando ne hai già assegnato uno, rispetto a quando non ne hai assegnato uno.
user541686

1
Penso che una risposta migliore sia che smart_ptr perderà se viene chiamata un'eccezione e nulla la cattura.
Natalie Adams,

2
Anche nel caso post-C ++ 17, può ancora verificarsi una perdita se ha esito newpositivo e quindi la shared_ptrcostruzione successiva non riesce. std::make_shared()risolverebbe anche questo
Remy Lebeau,

1
@Mehrdad il shared_ptrcostruttore in questione alloca memoria per un blocco di controllo che memorizza il puntatore e il deleter condivisi, quindi sì, può teoricamente lanciare un errore di memoria. Solo i costruttori di copia, spostamento e aliasing non vengono generati. make_sharedalloca l'oggetto condiviso all'interno del blocco di controllo stesso, quindi c'è solo 1 allocazione invece di 2.
Remy Lebeau

17

In larga misura, è qualcuno che eleva le proprie debolezze a una regola generale. Di per sé non c'è nulla di sbagliato nel creare oggetti usando l' newoperatore. Ciò per cui c'è qualche argomento è che devi farlo con un po 'di disciplina: se crei un oggetto devi assicurarti che verrà distrutto.

Il modo più semplice per farlo è creare l'oggetto nell'archiviazione automatica, quindi C ++ sa distruggerlo quando esce dall'ambito:

 {
    File foo = File("foo.dat");

    // do things

 }

Ora, osserva che quando cadi da quel blocco dopo il controvento, fooè fuori portata. C ++ chiamerà il suo dtor automaticamente per te. A differenza di Java, non è necessario attendere che il GC lo trovi.

Avevi scritto

 {
     File * foo = new File("foo.dat");

vorresti abbinarlo esplicitamente con

     delete foo;
  }

o meglio ancora, alloca il tuo File * come "puntatore intelligente". Se non stai attento, ciò può causare perdite.

La risposta stessa fa l'erroneo presupposto che se non si utilizza newnon si alloca sull'heap; in effetti, in C ++ non lo sai. Al massimo, sai che una piccola quantità di memoria, ad esempio un puntatore, è sicuramente allocata nello stack. Tuttavia, considera se l'implementazione di File è qualcosa di simile

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

quindi FileImplverrà comunque allocato nello stack.

E sì, è meglio essere sicuri di averlo

     ~File(){ delete fd ; }

anche in classe; senza di essa, perderai memoria dall'heap anche se apparentemente non hai allocato affatto sull'heap.


4
Dovresti dare un'occhiata al codice nella domanda di riferimento. Ci sono sicuramente molte cose che non vanno in quel codice.
André Caron,

7
Sono d'accordo che non c'è nulla di sbagliato nell'uso di new per sé , ma se si guarda al codice originale al quale si riferiva il commento, newviene abusato. Il codice è scritto come se fosse Java o C #, dove newviene utilizzato praticamente per ogni variabile, quando le cose hanno molto più senso essere nello stack.
Luca,

5
Punto valido. Ma le regole generali vengono normalmente applicate per evitare insidie ​​comuni. Che si tratti di una debolezza individuale o meno, la gestione della memoria è abbastanza complessa da giustificare una regola generale come questa! :)
Robben_Ford_Fan_boy il

9
@Charlie: il commento non dice che non dovresti mai usare new. Si dice che se si ha la scelta tra l'allocazione dinamica e memorizzazione automatica, utilizzare l'archiviazione automatica.
André Caron,

8
@Charlie: non c'è niente di sbagliato nell'utilizzare new, ma se lo usi delete, lo stai facendo male!
Matthieu M.,

16

new()non dovrebbe essere usato il meno possibile. Dovrebbe essere usato il più attentamente possibile. E dovrebbe essere usato tutte le volte che è necessario come dettato dal pragmatismo.

L'allocazione di oggetti in pila, basandosi sulla loro distruzione implicita, è un modello semplice. Se l'ambito richiesto di un oggetto si adatta a quel modello, non è necessario utilizzarlo new(), con associati delete()e controllo dei puntatori NULL. Nel caso in cui abbiate molti oggetti di breve durata l'allocazione nello stack dovrebbe ridurre i problemi di frammentazione dell'heap.

Tuttavia, se la durata del tuo oggetto deve estendersi oltre l'ambito attuale, allora new()è la risposta giusta. Assicurati solo di prestare attenzione a quando e come chiami delete()e alle possibilità dei puntatori NULL, usando gli oggetti eliminati e tutti gli altri gotcha che derivano dall'uso dei puntatori.


9
"se la durata del tuo oggetto deve estendersi oltre l'ambito attuale, allora new () è la risposta giusta" ... perché non preferibilmente restituire in base al valore o accettare una variabile con ambito chiamante con non- constref o puntatore ...?
Tony Delroy,

2
@Tony: Sì, sì! Sono felice di sentire qualcuno che invoca referenze. Sono stati creati per prevenire questo problema.
Nathan Osman,

@TonyD ... o combinali: restituisce un puntatore intelligente in base al valore. In questo modo il chiamante e in molti casi (cioè dove make_shared/_uniqueè utilizzabile) la chiamata non ha mai bisogno newo delete. Questa risposta non tiene conto dei punti reali: (A) C ++ fornisce cose come RVO, spostare la semantica e parametri di output - il che spesso significa che la gestione della creazione di oggetti e l'estensione della vita restituendo memoria allocata dinamicamente diventa superflua e disattenta. (B) Anche nelle situazioni in cui è richiesta l'allocazione dinamica, lo stdlib fornisce involucri RAII che alleviano l'utente dai brutti dettagli interni.
underscore_d

14

Quando si utilizza nuovo, gli oggetti vengono allocati nell'heap. Viene generalmente utilizzato quando si prevede l'espansione. Quando dichiari un oggetto come,

Class var;

viene posizionato in pila.

Dovrai sempre chiamare distruggere sull'oggetto che hai inserito nell'heap con nuovo. Questo apre il potenziale per perdite di memoria. Gli oggetti posizionati in pila non sono soggetti a perdite di memoria!


2
+1 "[heap] generalmente utilizzato quando prevedi l'espansione", ad esempio aggiungendo a un'intuizione acuta std::stringo std::mapsì. La mia reazione iniziale è stata "ma anche molto comunemente per disaccoppiare la durata di un oggetto dall'ambito del codice di creazione", ma tornare davvero in base al valore o accettare valori con ambito chiamante per non constriferimento o puntatore è meglio per questo, tranne quando c'è "espansione" coinvolta pure. Ci sono altri usi del suono come i metodi di fabbrica però ...
Tony Delroy,

12

Un motivo notevole per evitare un uso eccessivo dell'heap è per le prestazioni, in particolare per le prestazioni del meccanismo di gestione della memoria predefinito utilizzato da C ++. Mentre l'allocazione può essere piuttosto rapida nel caso banale, fare molti newe deletesu oggetti di dimensioni non uniformi senza un ordine rigoroso porta non solo alla frammentazione della memoria, ma complica anche l'algoritmo di allocazione e può assolutamente distruggere le prestazioni in alcuni casi.

Questo è il problema che i pool di memoria sono stati creati per risolvere, consentendo di mitigare gli svantaggi intrinseci delle implementazioni di heap tradizionali, consentendo comunque di utilizzare l'heap come necessario.

Meglio ancora, comunque, per evitare del tutto il problema. Se riesci a metterlo in pila, allora fallo.


Puoi sempre allocare una quantità ragionevolmente enorme di memoria e quindi utilizzare il posizionamento new / delete se la velocità è un problema.
rxantos,

I pool di memoria devono evitare la frammentazione, accelerare la deallocazione (una deallocazione per migliaia di oggetti) e rendere la deallocazione più sicura.
Lothar,

10

Penso che il poster volesse dire You do not have to allocate everything on theheappiuttosto che il stack.

Fondamentalmente gli oggetti sono allocati nello stack (se le dimensioni dell'oggetto lo consentono, ovviamente) a causa del costo economico dell'allocazione dello stack, piuttosto che dell'allocazione basata sull'heap che comporta un bel po 'di lavoro da parte dell'allocatore e aggiunge verbosità perché quindi è necessario gestire i dati allocati sull'heap.


10

Tendo a non essere d'accordo con l'idea di usare il nuovo "troppo". Sebbene l'uso del poster originale del nuovo con le classi di sistema sia un po 'ridicolo. ( int *i; i = new int[9999];? davvero? int i[9999];è molto più chiaro.) Penso che sia quello che stava ottenendo la capra del commentatore.

Quando lavori con oggetti di sistema, è molto raro che tu abbia bisogno di più di un riferimento allo stesso oggetto esatto. Finché il valore è lo stesso, è tutto ciò che conta. E gli oggetti di sistema in genere non occupano molto spazio in memoria. (un byte per carattere, in una stringa). E se lo fanno, le librerie dovrebbero essere progettate per tenere conto di quella gestione della memoria (se sono scritte bene). In questi casi, (tutte tranne una o due delle notizie nel suo codice), il nuovo è praticamente inutile e serve solo a introdurre confusioni e potenziale di bug.

Quando lavori con le tue classi / oggetti, tuttavia (ad es. La classe Line del poster originale), allora devi iniziare a pensare a problemi come l'impronta della memoria, la persistenza dei dati, ecc. A questo punto, consentire più riferimenti allo stesso valore è inestimabile: consente costrutti come elenchi collegati, dizionari e grafici, in cui più variabili devono non solo avere lo stesso valore, ma fare riferimento allo stesso oggetto esatto in memoria. Tuttavia, la classe Line non ha nessuno di questi requisiti. Quindi il codice del poster originale non ha assolutamente bisogno di new.


di solito il nuovo / elimina lo userebbe quando non si conosce in anticipo la dimensione dell'array. Naturalmente std :: vector nasconde nuovo / elimina per te. Li usi ancora, ma attraverso std :: vector. Quindi al giorno d'oggi sarebbe usato quando non si conoscono le dimensioni dell'array e si desidera per qualche motivo evitare il sovraccarico di std :: vector (che è piccolo, ma esiste ancora).
rxantos,

When you're working with your own classes/objects... spesso non hai motivo di farlo! Una piccola parte delle domande frequenti riguarda i dettagli del design del contenitore da parte di programmatori esperti. In netto contrasto, una percentuale deprimente sono circa confusione di neofiti che non conoscono esiste la stdlib - o stanno attivamente dato incarichi terribili a 'programmazione' 'corsi', dove un tutor richiede che inutilmente reinventare la ruota - prima di aver ancora imparato cos'è una ruota e perché funziona. Promuovendo un'allocazione più astratta, C ++ può salvarci dall'infinito "segfault con elenco collegato" di C; per favore, cerchiamo di lasciarlo .
underscore_d

" l'uso del nuovo poster originale con le classi di sistema è un po 'ridicolo. ( int *i; i = new int[9999];? Davvero? int i[9999];è molto più chiaro.)" Sì, è più chiaro, ma per interpretare l'avvocato del diavolo, il tipo non è necessariamente una cattiva discussione. Con 9999 elementi, posso immaginare un sistema embedded compatto che non abbia abbastanza stack per 9999 elementi: 9999x4 byte è ~ 40 kB, x8 ~ 80 kB. Pertanto, potrebbe essere necessario che tali sistemi utilizzino l'allocazione dinamica, supponendo che la implementino utilizzando una memoria alternativa. Tuttavia, ciò potrebbe solo giustificare l'allocazione dinamica, no new; a vectorsarebbe la vera soluzione in quel caso
underscore_d

Concordo con @underscore_d - non è un ottimo esempio. Non aggiungerei 40.000 o 80.000 byte al mio stack proprio così. Probabilmente li assegnerei probabilmente sull'heap (con std::make_unique<int[]>()ovviamente).
einpoklum,

3

Due motivi:

  1. In questo caso non è necessario. Stai rendendo il tuo codice inutilmente più complicato.
  2. Alloca spazio sull'heap e significa che devi ricordartelo in deleteseguito, altrimenti causerà una perdita di memoria.

2

newè il nuovo goto.

Ricordiamo perché gotoè così insultato: mentre è un potente strumento di basso livello per il controllo del flusso, le persone spesso lo hanno usato in modi inutilmente complicati che rendevano difficile seguire il codice. Inoltre, i modelli più utili e più facili da leggere sono stati codificati in istruzioni di programmazione strutturate (ad es. forO while); l'effetto finale è che il codice in cui gotoè il modo appropriato è piuttosto raro, se sei tentato di scrivere goto, probabilmente stai facendo cose male (a meno che tu non sappia davvero cosa stai facendo).

newè simile: viene spesso utilizzato per rendere le cose inutilmente complicate e più difficili da leggere e i modelli di utilizzo più utili che possono essere codificati sono stati codificati in varie classi. Inoltre, se è necessario utilizzare nuovi modelli di utilizzo per i quali non esistono già classi standard, è possibile scrivere le proprie classi che le codificano!

Direi anche che newè peggio di goto, a causa della necessità di accoppiare newe deletedichiarazioni.

Ad esempio goto, se hai mai pensato di dover usare new, probabilmente stai facendo cose male, specialmente se lo stai facendo al di fuori dell'implementazione di una classe il cui scopo nella vita è incapsulare qualsiasi allocazione dinamica che devi fare.


E aggiungerei: "Fondamentalmente non ne hai bisogno".
einpoklum,

1

Il motivo principale è che gli oggetti nell'heap sono sempre difficili da usare e gestire rispetto ai semplici valori. Scrivere codici facili da leggere e mantenere è sempre la prima priorità di qualsiasi programmatore serio.

Un altro scenario è la libreria che stiamo utilizzando fornisce semantica di valore e rende superflua l'allocazione dinamica. Std::stringè un buon esempio.

Per il codice orientato agli oggetti, tuttavia, è necessario utilizzare un puntatore, che significa utilizzare newper crearlo in anticipo. Al fine di semplificare la complessità della gestione delle risorse, abbiamo dozzine di strumenti per renderlo il più semplice possibile, come i puntatori intelligenti. Il paradigma basato su oggetti o paradigma generico assume valore semantico e richiede meno o no new, proprio come indicato nei poster altrove.

I modelli di design tradizionali, in particolare quelli citati nel libro GoF , usano newmolto, in quanto sono tipici codici OO.


4
Questa è una risposta abissale . For object oriented code, using a pointer [...] is a must: sciocchezze . Se stai svalutando 'OO' facendo riferimento solo a un piccolo sottoinsieme, il polimorfismo - anche senza senso: anche i riferimenti funzionano. [pointer] means use new to create it beforehand: soprattutto senza senso : riferimenti o puntatori possono essere presi su oggetti allocati automaticamente e usati polimorficamente; guardare me . [typical OO code] use new a lot: forse in qualche vecchio libro, ma a chi importa? Qualsiasi evitamento del C ++ vagamente moderno new/ puntatori non elaborati ove possibile - e non è in alcun modo meno OO nel farlo
underscore_d

1

Un altro punto a tutte le risposte esatte sopra, dipende dal tipo di programmazione che stai facendo. Lo sviluppo del kernel in Windows, ad esempio -> Lo stack è fortemente limitato e potresti non essere in grado di accettare errori di pagina come in modalità utente.

In tali ambienti, le chiamate API nuove o simili a C sono preferite e persino richieste.

Naturalmente, questa è solo un'eccezione alla regola.


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.