Istanza di oggetti C ++


113

Sono un programmatore C che cerca di capire il C ++. Molti tutorial dimostrano la creazione di istanze di oggetti utilizzando uno snippet come:

Dog* sparky = new Dog();

il che implica che in seguito farai:

delete sparky;

il che ha senso. Ora, nel caso in cui l'allocazione dinamica della memoria non sia necessaria, c'è qualche motivo per utilizzare quanto sopra invece di

Dog sparky;

e lasciare che il distruttore venga chiamato una volta che Sparky esce dal campo di applicazione?

Grazie!

Risposte:


166

Al contrario, dovresti sempre preferire le allocazioni dello stack, nella misura in cui, come regola generale, non dovresti mai avere new / delete nel tuo codice utente.

Come dici tu, quando la variabile viene dichiarata nello stack, il suo distruttore viene chiamato automaticamente quando esce dall'ambito, che è il tuo strumento principale per tenere traccia della durata della risorsa ed evitare perdite.

Quindi, in generale, ogni volta che è necessario allocare una risorsa, sia che si tratti di memoria (chiamando new), handle di file, socket o qualsiasi altra cosa, racchiuderla in una classe in cui il costruttore acquisisce la risorsa e il distruttore la rilascia. Quindi puoi creare un oggetto di quel tipo nello stack e hai la garanzia che la tua risorsa venga liberata quando esce dall'ambito. In questo modo non devi tenere traccia delle tue nuove coppie / eliminare ovunque per assicurarti di evitare perdite di memoria.

Il nome più comune per questo idioma è RAII

Esamina anche le classi di puntatori intelligenti che vengono utilizzate per avvolgere i puntatori risultanti nei rari casi in cui devi allocare qualcosa con nuovo al di fuori di un oggetto RAII dedicato. Si passa invece il puntatore a un puntatore intelligente, che quindi tiene traccia della sua durata, ad esempio mediante il conteggio dei riferimenti, e chiama il distruttore quando l'ultimo riferimento esce dall'ambito. La libreria standard ha std::unique_ptruna semplice gestione basata sull'ambito e std::shared_ptrfa riferimento al conteggio per implementare la proprietà condivisa.

Molti tutorial dimostrano la creazione di istanze di oggetti utilizzando uno snippet come ...

Quindi quello che hai scoperto è che la maggior parte dei tutorial fa schifo. ;) La maggior parte dei tutorial ti insegna le pessime pratiche C ++, incluso chiamare new / delete per creare variabili quando non è necessario e darti difficoltà a tenere traccia della durata delle tue allocazioni.


I puntatori non elaborati sono utili quando si desidera una semantica simile a auto_ptr (trasferimento della proprietà), ma si mantiene l'operazione di scambio senza lancio e non si desidera l'overhead del conteggio dei riferimenti. Custodia Edge forse, ma utile.
Greg Rogers

2
Questa è una risposta corretta, ma il motivo per cui non prenderei mai l'abitudine di creare oggetti in pila è perché non è mai del tutto ovvio quanto sarà grande quell'oggetto. Stai solo chiedendo un'eccezione di overflow dello stack.
dviljoen

3
Greg: Certamente. Ma come dici tu, un caso limite. In generale, è meglio evitare i puntatori. Ma sono nella lingua per un motivo, non lo neghiamo. :) dviljoen: Se l'oggetto è grande, lo racchiudi in un oggetto RAII, che può essere allocato sullo stack e contiene un puntatore ai dati allocati nell'heap.
jalf

3
@dviljoen: No, non lo sono. I compilatori C ++ non gonfiano inutilmente gli oggetti. La cosa peggiore che vedrai è in genere che viene arrotondato al multiplo di quattro byte più vicino. Di solito, una classe contenente un puntatore occupa tanto spazio quanto il puntatore stesso, quindi non ti costa nulla nell'utilizzo dello stack.
jalf

20

Sebbene avere cose in pila possa essere un vantaggio in termini di allocazione e liberazione automatica, presenta alcuni svantaggi.

  1. Potresti non voler allocare oggetti enormi nello Stack.

  2. Spedizione dinamica! Considera questo codice:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Questo stamperà "B". Ora vediamo cosa succede quando si utilizza Stack:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

In questo modo verrà stampata una "A", che potrebbe non essere intuitiva per coloro che hanno familiarità con Java o altri linguaggi orientati agli oggetti. Il motivo è che non hai più un puntatore a un'istanza di B. Viene invece Bcreata un'istanza di e copiata nella avariabile di tipo A.

Alcune cose potrebbero accadere in modo non intuitivo, specialmente quando sei nuovo in C ++. In C hai i tuoi suggerimenti e basta. Sai come usarli e fanno SEMPRE lo stesso. In C ++ questo non è il caso. Immagina solo cosa succede, quando usi un in questo esempio come argomento per un metodo: le cose si complicano e FA un'enorme differenza se aè di tipo Ao A*o addirittura A&(call-by-reference). Sono possibili molte combinazioni e tutte si comportano diversamente.


2
-1: le persone che non sono in grado di comprendere la semantica del valore e il semplice fatto che il polimorfismo necessita di un riferimento / puntatore (per evitare il taglio di oggetti) non costituiscono alcun tipo di problema con il linguaggio. Il potere di c ++ non dovrebbe essere considerato un inconveniente solo perché alcune persone non possono apprenderne le regole di base.
underscore_d

Grazie per il testa a testa. Ho avuto un problema simile con il metodo che accettava l'oggetto invece del puntatore o del riferimento, e che casino era. L'oggetto ha dei puntatori all'interno e sono stati eliminati troppo presto a causa di questo coping.
BoBoDev

@underscore_d Accetto; l'ultimo paragrafo di questa risposta dovrebbe essere rimosso. Non prendere il fatto che ho modificato questa risposta per significare che sono d'accordo con essa; Semplicemente non volevo che gli errori in esso fossero letti.
TamaMcGlinn

@TamaMcGlinn è d'accordo. Grazie per la buona modifica. Ho rimosso la parte di opinione.
Universo

13

Ebbene, il motivo per utilizzare il puntatore sarebbe esattamente lo stesso del motivo per cui utilizzare i puntatori in C allocati con malloc: se vuoi che il tuo oggetto viva più a lungo della tua variabile!

Si consiglia vivamente di NON utilizzare il nuovo operatore se è possibile evitarlo. Soprattutto se usi le eccezioni. In generale è molto più sicuro lasciare che il compilatore liberi i tuoi oggetti.


13

Ho visto questo anti-pattern da persone che non ottengono abbastanza l'operatore & address-of. Se hanno bisogno di chiamare una funzione con un puntatore, lo allocano sempre sull'heap in modo da ottenere un puntatore.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);

In alternativa: void FeedTheDog (Dog & hungryDog); Cane Cane; FeedTheDog (cane);
Scott Langham

1
@ScottLangham vero, ma non era questo il punto che stavo cercando di sottolineare. A volte stai chiamando una funzione che accetta un puntatore NULL opzionale, ad esempio, o forse è un'API esistente che non può essere modificata.
Mark Ransom

7

Tratta l'heap come un bene immobile molto importante e usalo con molta saggezza. La regola empirica di base è usare lo stack ogni volta che è possibile e utilizzare l'heap ogni volta che non c'è altro modo. Assegnando gli oggetti in pila puoi ottenere molti vantaggi come:

(1). Non devi preoccuparti dello svolgimento dello stack in caso di eccezioni

(2). Non è necessario preoccuparsi della frammentazione della memoria causata dall'allocazione di più spazio del necessario da parte del gestore di heap.


1
Ci dovrebbe essere una certa considerazione per quanto riguarda le dimensioni dell'oggetto. Lo Stack ha dimensioni limitate, quindi gli oggetti molto grandi dovrebbero essere allocati nell'heap.
Adam

1
@Adam, penso che Naveen abbia ancora ragione qui. Se puoi mettere un oggetto di grandi dimensioni in pila, fallo, perché è meglio. Ci sono molte ragioni per cui probabilmente non puoi. Ma penso che abbia ragione.
McKay

5

L'unico motivo di cui mi preoccuperei è che Dog ora è allocato nello stack, piuttosto che nell'heap. Quindi, se Dog ha una dimensione di megabyte, potresti avere un problema,

Se hai bisogno di seguire il percorso nuovo / elimina, fai attenzione alle eccezioni. E per questo motivo dovresti usare auto_ptr o uno dei tipi di puntatori intelligenti boost per gestire la durata dell'oggetto.


1

Non c'è motivo di nuovo (sull'heap) quando puoi allocare sullo stack (a meno che per qualche motivo tu abbia uno stack piccolo e desideri usare l'heap.

Potresti prendere in considerazione l'utilizzo di shared_ptr (o una delle sue varianti) dalla libreria standard se vuoi allocare sull'heap. Questo gestirà l'eliminazione per te una volta che tutti i riferimenti a shared_ptr saranno scomparsi.


Ci sono molte ragioni, per esempio, vedimi rispondere.
dangerousdave

Suppongo che potresti volere che l'oggetto sopravviva allo scopo della funzione, ma l'OP non sembra suggerire che è quello che stavano cercando di fare.
Scott Langham

0

C'è un motivo in più, che nessun altro ha menzionato, per cui potresti scegliere di creare il tuo oggetto dinamicamente. Gli oggetti dinamici basati su heap consentono di utilizzare il polimorfismo .


2
Puoi lavorare con oggetti basati sullo stack anche polimorficamente, non vedo alcuna differenza tra oggetti allocati stack / e heap a questo proposito. Ad esempio: void MyFunc () {Dog dog; Dai da mangiare al cane); } void Feed (Animal & animal) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (cibo); } // In questo esempio GetFavouriteFood può essere una funzione virtuale che ogni animale sostituisce con la propria implementazione. Questo funzionerà in modo polimorfico senza coinvolgere heap.
Scott Langham

-1: il polimorfismo richiede solo un riferimento o un puntatore; è totalmente indipendente dalla durata di archiviazione dell'istanza sottostante.
underscore_d

@underscore_d "L'uso di una classe base universale implica un costo: gli oggetti devono essere allocati nell'heap per essere polimorfici;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave

LOL, non mi interessa se lo ha detto lo stesso Stroustrup, ma è una citazione incredibilmente imbarazzante per lui, perché ha torto. Non è difficile testarlo da soli, sai: istanziare alcuni DerivedA, DerivedB e DerivedC nello stack; istanzia anche un puntatore allocato nello stack a Base e conferma che puoi posizionarlo con A / B / C e usarli polimorficamente. Applicare il pensiero critico e il riferimento standard alle affermazioni, anche se sono dell'autore originale del linguaggio. Qui: stackoverflow.com/q/5894775/2757035
underscore_d

Mettiamola così, ho un oggetto contenente 2 famiglie separate di quasi 1000 ciascuna di oggetti polimorfici, con durata di memorizzazione automatica. Istanzio questo oggetto sullo stack e, facendo riferimento a quei membri tramite riferimenti o puntatori, si ottiene la piena capacità di polimorfismo su di essi. Niente nel mio programma è allocato dinamicamente / heap (a parte le risorse di proprietà delle classi allocate nello stack) e non cambia di una virgola le capacità del mio oggetto.
underscore_d

-4

Ho avuto lo stesso problema in Visual Studio. Devi usare:

yourClass-> classmethod ();

piuttosto che:

yourClass.classMethod ();


3
Questo non risponde alla domanda. Si noti piuttosto che è necessario utilizzare una sintassi diversa per accedere all'oggetto allocato sull'heap (tramite il puntatore all'oggetto) e all'oggetto allocato sullo stack.
Alexey Ivanov
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.