Perché l'uso di "nuovo" causa perdite di memoria?


131

Ho imparato prima C #, e ora sto iniziando con C ++. A quanto ho capito, l'operatore newin C ++ non è simile a quello in C #.

Puoi spiegare il motivo della perdita di memoria in questo codice di esempio?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

Risposte:


464

Che cosa sta succedendo

Quando scrivi T t;stai creando un oggetto di tipo Tcon durata di memorizzazione automatica . Verrà ripulito automaticamente quando esce dal campo di applicazione.

Quando scrivi new T()stai creando un oggetto di tipo Tcon durata di archiviazione dinamica . Non verrà pulito automaticamente.

nuovo senza pulizia

È necessario passare un puntatore ad esso deleteper ripulirlo:

novità con elimina

Tuttavia, il tuo secondo esempio è peggio: stai dereferenziando il puntatore e stai facendo una copia dell'oggetto. In questo modo perdi il puntatore all'oggetto creato con new, così non potrai mai cancellarlo anche se lo desideri!

novità con deref

Cosa dovresti fare

Dovresti preferire la durata della memorizzazione automatica. Hai bisogno di un nuovo oggetto, basta scrivere:

A a; // a new object of type A
B b; // a new object of type B

Se è necessaria una durata della memoria dinamica, archiviare il puntatore sull'oggetto allocato in un oggetto durata della memoria automatica che lo elimina automaticamente.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

novità con automatic_pointer

Questo è un linguaggio comune che va dal nome non molto descrittivo RAII ( Resource Acquisition Is Initialization ). Quando si acquisisce una risorsa che necessita di pulizia, la si inserisce in un oggetto con durata di archiviazione automatica, quindi non è necessario preoccuparsi di ripulirla. Questo vale per qualsiasi risorsa, sia essa memoria, file aperti, connessioni di rete o qualunque cosa tu voglia.

Questo automatic_pointer cosa esiste già in varie forme, l'ho appena fornita per fare un esempio. Una classe molto simile esiste nella libreria standard chiamata std::unique_ptr.

Ce n'è anche uno vecchio (pre-C ++ 11) chiamato auto_ptr ma ora è deprecato perché ha uno strano comportamento di copia.

E poi ci sono alcuni esempi ancora più intelligenti, come std::shared_ptr, che consente a più puntatori allo stesso oggetto e lo pulisce solo quando l'ultimo puntatore viene distrutto.


4
@ user1131997: felice di aver fatto un'altra domanda. Come puoi vedere non è molto facile da spiegare nei commenti :)
R. Martinho Fernandes,

@ R.MartinhoFernandes: ottima risposta. Solo una domanda. Perché hai usato return per riferimento nella funzione operator * ()?
Distruttore

@ Risposta in ritardo distruttore: D. La restituzione per riferimento consente di modificare la punta, in modo da poter fare, ad esempio *p += 2, come si farebbe con un normale puntatore. Se non ritornasse per riferimento, non imiterebbe il comportamento di un normale puntatore, che è l'intenzione qui.
R. Martinho Fernandes,

Grazie mille per aver consigliato di "archiviare il puntatore sull'oggetto allocato in un oggetto durata della memorizzazione automatica che lo elimina automaticamente". Se solo ci fosse un modo per richiedere ai programmatori di apprendere questo modello prima che siano in grado di compilare qualsiasi C ++!
Andy,

35

Una spiegazione dettagliata:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

Quindi, alla fine, hai un oggetto nell'heap senza puntatore, quindi è impossibile eliminarlo.

L'altro campione:

A *object1 = new A();

è una perdita di memoria solo se si dimentica deletela memoria allocata:

delete object1;

In C ++ ci sono oggetti con memoria automatica, quelli creati nello stack, che vengono automaticamente eliminati e oggetti con memoria dinamica, nell'heap, che si allocano con newe sono tenuti a liberarsi condelete . (questo è tutto sommato)

Pensa che dovresti avere un deleteper ogni oggetto allocato con new.

MODIFICARE

Vieni a pensarci bene, object2non deve essere una perdita di memoria.

Il seguente codice è solo per fare un punto, è una cattiva idea, non ti piace mai un codice come questo:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

In questo caso, poiché otherviene passato per riferimento, sarà l'oggetto esatto a cui punta new B(). Pertanto, ottenere il suo indirizzo da&other ed eliminare il puntatore libererebbe la memoria.

Ma non posso sottolineare abbastanza, non farlo. È qui solo per fare un punto.


2
Stavo pensando lo stesso: possiamo hackerarlo per non perdere ma non vorrai farlo. neanche oggetto1 deve perdere, poiché il suo costruttore potrebbe collegarsi a un qualche tipo di struttura di dati che lo eliminerà ad un certo punto.
CashCow,

2
È sempre così allettante scrivere quelle risposte "è possibile farlo ma non"! :-) Conosco la sensazione
Kos

11

Dati due "oggetti":

obj a;
obj b;

Non occuperanno la stessa posizione in memoria. In altre parole,&a != &b

Assegnare il valore dell'uno all'altro non cambierà la loro posizione, ma cambierà il loro contenuto:

obj a;
obj b = a;
//a == b, but &a != &b

Intuitivamente, gli "oggetti" del puntatore funzionano allo stesso modo:

obj *a;
obj *b = a;
//a == b, but &a != &b

Ora diamo un'occhiata al tuo esempio:

A *object1 = new A();

Questo sta assegnando il valore di new A()a object1. Il valore è un puntatore, che significa object1 == new A(), ma &object1 != &(new A()). (Nota che questo esempio non è un codice valido, è solo per spiegazione)

Poiché il valore del puntatore viene preservato, possiamo liberare la memoria a cui punta: a delete object1;causa della nostra regola, ciò si comporta come se delete (new A());non ci fossero perdite.


Per il tuo secondo esempio, stai copiando l'oggetto puntato. Il valore è il contenuto di quell'oggetto, non il puntatore effettivo. Come in ogni altro caso &object2 != &*(new A()),.

B object2 = *(new B());

Abbiamo perso il puntatore alla memoria allocata e quindi non possiamo liberarlo. delete &object2;può sembrare che funzioni, ma perché &object2 != &*(new A())non è equivalente delete (new A())e quindi non valido.


9

In C # e Java, usi new per creare un'istanza di qualsiasi classe e quindi non devi preoccuparti di distruggerla in seguito.

C ++ ha anche una parola chiave "nuovo" che crea un oggetto ma a differenza di Java o C #, non è l'unico modo per creare un oggetto.

C ++ ha due meccanismi per creare un oggetto:

  • automatico
  • dinamico

Con la creazione automatica si crea l'oggetto in un ambiente con ambito: - in una funzione o - come membro di una classe (o struttura).

In una funzione lo creeresti in questo modo:

int func()
{
   A a;
   B b( 1, 2 );
}

All'interno di una classe normalmente lo creeresti in questo modo:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

Nel primo caso, gli oggetti vengono distrutti automaticamente all'uscita dal blocco dell'ambito. Potrebbe essere una funzione o un blocco dell'ambito all'interno di una funzione.

In quest'ultimo caso l'oggetto b viene distrutto insieme all'istanza di A in cui è un membro.

Gli oggetti vengono allocati con nuovi quando è necessario controllare la durata dell'oggetto e quindi richiede l'eliminazione per distruggerlo. Con la tecnica nota come RAII, ti occupi della cancellazione dell'oggetto nel momento in cui lo crei inserendolo in un oggetto automatico e attendi che il distruttore di quell'oggetto automatico abbia effetto.

Uno di questi oggetti è shared_ptr che invocherà una logica "deleter" ma solo quando tutte le istanze di shared_ptr che condividono l'oggetto vengono distrutte.

In generale, mentre il tuo codice potrebbe avere molte chiamate a nuovo, dovresti avere chiamate limitate da eliminare e assicurarti sempre che vengano chiamate da distruttori o oggetti "deleter" che vengono inseriti in smart-pointer.

Anche i distruttori non dovrebbero mai generare eccezioni.

Se lo fai, avrai poche perdite di memoria.


4
C'è più di automatice dynamic. C'è anche static.
Mooing Duck

9
B object2 = *(new B());

Questa linea è la causa della perdita. Scegliamo un po 'questo ..

object2 è una variabile di tipo B, memorizzata a dire indirizzo 1 (Sì, sto selezionando numeri arbitrari qui). Sul lato destro, hai chiesto una nuova B o un puntatore a un oggetto di tipo B. Il programma ti dà volentieri e ti assegna la tua nuova B all'indirizzo 2 e crea anche un puntatore all'indirizzo 3. Ora, L'unico modo per accedere ai dati nell'indirizzo 2 è tramite il puntatore nell'indirizzo 3. Successivamente, hai indicato il puntatore usando* per ottenere i dati a cui punta il puntatore (i dati nell'indirizzo 2). Questo crea effettivamente una copia di quei dati e li assegna a object2, assegnato nell'indirizzo 1. Ricorda, è una COPIA, non l'originale.

Ora, ecco il problema:

Non hai mai memorizzato quel puntatore ovunque tu possa usarlo! Una volta terminata questa assegnazione, il puntatore (memoria nell'indirizzo3, che hai usato per accedere all'indirizzo2) è fuori portata e fuori dalla tua portata! Non è più possibile chiamare delete su di esso e quindi non è possibile ripulire la memoria in address2. Ciò che ti rimane è una copia dei dati da address2 in address1. Due delle stesse cose che si trovano nella memoria. Uno a cui puoi accedere, l'altro a cui non puoi (perché hai perso il percorso). Ecco perché questa è una perdita di memoria.

Suggerirei di venire dal tuo background in C # di leggere molto su come funzionano i puntatori in C ++. Sono un argomento avanzato e possono richiedere del tempo per essere compreso, ma il loro uso sarà prezioso per te.


8

Se rende più semplice, pensa alla memoria del computer come a un hotel e i programmi sono clienti che assumono stanze quando ne hanno bisogno.

Il modo in cui funziona questo hotel è che prenoti una stanza e comunichi al portiere quando parti.

Se si programma di prenotare una stanza e se ne va senza dirlo al portiere, il portiere penserà che la stanza è ancora in uso e non permetterà a nessun altro di usarla. In questo caso c'è una perdita di spazio.

Se il tuo programma alloca memoria e non la elimina (smette semplicemente di usarla), il computer pensa che la memoria sia ancora in uso e non consentirà a nessun altro di usarla. Questa è una perdita di memoria.

Questa non è un'analogia esatta ma potrebbe aiutare.


5
Mi piace abbastanza quell'analogia, non è perfetta, ma è sicuramente un buon modo per spiegare le perdite di memoria alle persone che ne sono nuove!
AdamM,

1
L'ho usato in un'intervista per un ingegnere senior a Bloomberg a Londra per spiegare perdite di memoria a una ragazza delle risorse umane. Ho superato l'intervista perché sono stato in grado di spiegare le perdite di memoria (e i problemi di threading) a un non programmatore in un modo che lei ha capito.
Stefan,

7

Quando crei object2stai creando una copia dell'oggetto che hai creato con nuovo, ma stai anche perdendo il puntatore (mai assegnato) (quindi non c'è modo di eliminarlo in seguito). Per evitare ciò, dovresti fare object2un riferimento.


3
È una cattiva pratica prendere l'indirizzo di un riferimento per eliminare un oggetto. Usa un puntatore intelligente.
Tom Whittock,

3
Incredibilmente cattiva pratica, eh? Cosa pensi che i puntatori intelligenti utilizzino dietro le quinte?
Blindy,

3
I puntatori intelligenti di @Blindy (almeno quelli decentemente implementati) usano direttamente i puntatori.
Luchian Grigore,

2
Beh, ad essere sinceri, l'intera Idea non è eccezionale, vero? In realtà, non sono nemmeno sicuro di dove il modello provato nell'OP sarebbe effettivamente utile.
Mario,

7

Bene, crei una perdita di memoria se a un certo punto non liberi la memoria che hai allocato usando l' newoperatore passando un puntatore a quella memoria aldelete all'operatore.

Nei due casi precedenti:

A *object1 = new A();

Qui non stai usando deleteper liberare la memoria, quindi se e quando il tuo object1puntatore esce dall'ambito, avrai una perdita di memoria, perché avrai perso il puntatore e quindi non puoi usare ildelete operatore su di esso.

E qui

B object2 = *(new B());

stai eliminando il puntatore restituito da new B()e quindi non puoi mai passare quel puntatore deleteper liberare la memoria. Da qui un'altra perdita di memoria.


7

È questa linea che perde immediatamente:

B object2 = *(new B());

Qui stai creando un nuovo B oggetto nell'heap, quindi creando una copia nello stack. Non è più possibile accedere a quello assegnato all'heap e quindi alla perdita.

Questa linea non perde immediatamente:

A *object1 = new A();

Ci sarebbe una perdita se non l'avessi mai deletepensato object1.


4
Si prega di non utilizzare heap / stack quando si spiega l'archiviazione dinamica / automatica.
Pubblico il

2
@Pubby perché non usare? A causa dell'archiviazione dinamica / automatica è sempre heap, non stack? Ed è per questo che non c'è bisogno di dettagli su stack / heap, vero?

4
@utente1131997 L'heap / stack sono dettagli di implementazione. Sono importanti da sapere, ma sono irrilevanti per questa domanda.
Pubblico il

2
Vorrei una risposta separata, vale a dire la mia, ma sostituendo l'heap / stack con quello che pensi meglio. Sarei interessato a scoprire come preferiresti spiegarlo.
Mattjgalloway,
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.