2010-08-17 7 views
10

Sto cercando di capire qualcosa in C++. Fondamentalmente ho questo:L'oggetto è già inizializzato sulla dichiarazione?

class SomeClass { 
    public: 
     SomeClass(); 
    private: 
     int x; 
}; 

SomeClass::SomeClass(){ 
    x = 10; 
} 

int main() { 
    SomeClass sc; 
    return 0; 
} 

ho pensato che SC è una variabile non inizializzata di tipo SomeClass, ma dai vari tutorial che ho trovato sembra che questa dichiarazione è in realtà un'inizializzazione che chiama la contructor SomeClass(), senza di me aver bisogno di chiamare "sc = new SomeClass();" o qualcosa di simile.

Mentre vengo dal mondo C# (e conosco un po 'C, ma non C++), sto cercando di capire quando ho bisogno di cose come nuove e quando rilasciare oggetti del genere. Ho trovato uno schema chiamato RAll che sembra non essere correlato.

Come viene chiamato questo tipo di inizializzazione e come faccio a sapere se qualcosa è una semplice dichiarazione o un'inizializzazione completa?

+6

"Come vengo dal mondo C#" Allora? C++ non è C#, dimentica di sapere C# a tutti. Hai bisogno di un [buon libro per principianti] (http://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list) e inizi da zero. – GManNickG

+0

Grazie per l'elenco! Vero, C++ e C# sono due lingue molto diverse. Immagino che non voglio leggere su quali loop o classi sono e su come funzionano in C++, quindi ho evitato libri. Guardo la lista. –

+0

@ Michael: non puoi evitare libri. :) C++ è la propria lingua. Sembra che tu pensi di capire già le classi C++, ma non sono la stessa cosa di C#. Certo, potresti già sapere quali sono le istruzioni di selezione o di iterazione, ma questo è solo un capitolo. – GManNickG

risposta

17

Penso che ci sono diverse cose qui:

  • Differenza tra variabile automatica e allocata dinamicamente variabile
  • durata degli oggetti
  • RAII
  • C# parallelo

Automatico vs Dinamico

Una variabile automatica è una variabile di cui il sistema gestirà la durata. Diamo fosso variabili globali in questo momento, è complicato, e concentrarsi sul caso solita:

int main(int argc, char* argv[]) // 1 
{         // 2 
    SomeClass sc;     // 3 
    sc.foo();      // 4 
    return 0;      // 5 
}         // 6 

Qui sc è una variabile automatica. È garantito che sia completamente inizializzato (ovvero che il costruttore abbia la garanzia dell'esecuzione) dopo che l'esecuzione della riga (3) è stata completata correttamente. Il suo distruttore verrà automaticamente richiamato sulla linea (6).

Generalmente parliamo dell'ambito di una variabile: dal punto di dichiarazione alla parentesi di chiusura corrispondente; e la lingua garantisce la distruzione quando l'oscilloscopio verrà chiuso, sia esso con uno return o un'eccezione.

Naturalmente non vi è alcuna garanzia nel caso in cui si invochi il temuto "comportamento non definito" che generalmente si traduce in un arresto anomalo.

D'altra parte, C++ ha anche variabili dinamiche, cioè variabili che vengono allocate utilizzando new.

int main(int argc, char* argv[]) // 1 
{         // 2 
    SomeClass* sc = 0;    // 3 
    sc = new SomeClass();   // 4 
    sc->foo();      // 5 
    return 0;      // 6 
}         // 7 (!! leak) 

Qui sc è ancora una variabile automatica, tuttavia il tipo differisce: è ora un puntatore a una variabile di tipo SomeClass.

In linea (3) sc viene assegnato un valore di puntatore nullo (nullptr in C++ 0x) perché non punta a nessuna istanza di SomeClass. Si noti che la lingua non garantisce alcuna inizializzazione autonomamente, quindi è necessario assegnare in modo esplicito qualcosa altrimenti si avrà un valore spazzatura.

Sulla linea (4) costruiamo una variabile dinamica (utilizzando l'operatore new) e assegniamo il suo indirizzo a sc. Si noti che la variabile dinamica stessa è senza nome, il sistema ci fornisce solo un puntatore (indirizzo).

In linea (7) il sistema distrugge automaticamente sc, tuttavia non distrugge la variabile dinamica a cui punta, e quindi ora abbiamo una variabile dinamica il cui indirizzo non è memorizzato da nessuna parte. A meno che non usiamo un garbage collector (che non è il caso in C++ standard), abbiamo quindi perso memoria perché la memoria della variabile non sarà recuperata prima che il processo finisca ... e anche allora il distruttore non verrà eseguito (peccato se avesse effetti collaterali).

durata degli oggetti

Herb Sutter ha molto interessanti articoli su questo soggetto. Ecco the first.

In sintesi:

  • Un oggetto vive appena il costruttore viene eseguito fino al completamento. Significa che se il costruttore lancia, l'oggetto non è mai vissuto (consideralo un incidente di gravidanza).
  • Un oggetto è morto non appena viene richiamato il suo distruttore, se il distruttore getta (questo è EVIL) non può essere tentato di nuovo perché non è possibile richiamare alcun metodo su un oggetto morto, è un comportamento non definito.

Se torniamo al primo esempio:

int main(int argc, char* argv[]) // 1 
{         // 2 
    SomeClass sc;     // 3 
    sc.foo();      // 4 
    return 0;      // 5 
}         // 6 

sc è vivo dalla linea (4) per la linea (5) compreso. In linea (3) è in costruzione (che può fallire per un numero qualsiasi di ragioni) e in linea (6) viene distrutto.

RAII

RAII significa Risorse acquisizione è di inizializzazione. È un idioma per gestire le risorse, e in particolare per essere sicuro che le risorse verranno alla fine rilasciate una volta acquisite.

In C++, dal momento che non abbiamo garbage collection, questo idioma è applicata principalmente alla gestione della memoria, ma è utile anche per qualsiasi altro tipo di risorse: serrature in ambienti multithread, file serrature, prese/connessioni in rete, ecc ...

Se utilizzato per la gestione della memoria, viene utilizzato per accoppiare la durata della variabile dinamica alla durata di un determinato insieme di variabili automatiche, assicurando che la variabile dinamica non sopravviva (e vada persa).

Nella sua forma più semplice, è accoppiato ad una singola variabile automatica:

int main(int argc, char* argv[]) 
{ 
    std::unique_ptr<SomeClass> sc = new SomeClass(); 
    sc->foo(); 
    return 0; 
} 

è molto simile al primo esempio, tranne che allocare dinamicamente un'istanza di SomeClass. L'indirizzo di questa istanza viene quindi passato all'oggetto sc, di tipo std::unique_ptr<SomeClass> (è una funzione C++ 0x, se non disponibile è disponibile boost::scoped_ptr). unique_ptr garantisce che l'oggetto puntato verrà distrutto quando sc viene distrutto.

In una forma più complessa, potrebbe essere accoppiato a diverse variabili automatiche utilizzando (ad esempio) std::shared_ptr, che come suggerisce il nome consente di condividere un oggetto e garantisce che l'oggetto verrà distrutto quando l'ultimo condivisore viene distrutto. Fai attenzione che questo non è equivalente all'utilizzo di un garbage collector e ci possono essere problemi con cicli di riferimenti, non approfondirò qui, quindi ricorda che lo std::shared_ptr non è una panacea.

Perché è molto complicato da gestire perfettamente la durata di una variabile dinamica senza RAII di fronte alle eccezioni e il codice multi-threaded, la raccomandazione è:

  • uso variabili automatiche, per quanto possibile
  • per dinamica variabili, non invocano delete sul proprio e fa sempre uso delle strutture RAII

considero personalmente qualsiasi occorrenza di delete ad essere fortemente sospetto, e ho sempre puntuale Chiediamo la sua rimozione nelle recensioni del codice: è un odore di codice.

C# parallelo

In C# si utilizza principalmente variabili dinamiche *. Questo è il motivo per cui:

  • Se hai appena dichiara una variabile, senza assegnazione, il suo valore è nullo: in sostanza si stanno manipolando solo puntatori e si ha quindi un puntatore nullo (inizializzazione è garantito, grazie al cielo)
  • Usa new per creare valori, invoca il costruttore del tuo oggetto e ti fornisce l'indirizzo dell'oggetto; si noti come la sintassi è simile a C++ per le variabili dinamiche

Tuttavia, a differenza di C++, C# è spazzatura raccolti in modo da non dovete preoccuparvi di gestione della memoria.

Essere garbage collection significa anche che la vita degli oggetti è più difficile da capire: sono costruiti quando li chiedi ma sono distrutti a discrezione del sistema. Questo può essere un problema per implementare RAII, ad esempio se si desidera realmente rilasciare il blocco rapidamente e la lingua dispone di numerose funzionalità che consentono di semplificare l'interfaccia using parola chiave + IDisposable dalla memoria.

*: è facile controllare, se dopo aver dichiarato una variabile il suo valore è null, allora sarà una variabile dinamica. Credo che per il int il valore sarà 0 che indica che non lo è, ma sono passati 3 anni da quando ho manipolato C# per un progetto di corso quindi ...

3

Quello che stai facendo nella prima riga di main() è di allocare un oggetto SomeClass nello stack. L'operatore new invece assegna gli oggetti all'heap, restituendo un puntatore all'istanza della classe. Questo conduce infine alle due diverse tecniche di accesso attraverso il . (ad esempio) o con il -> (con il puntatore)

Poiché si sa C, si eseguono allocazione pila ogni volta che si dice, per esempio int i;. D'altra parte, l'allocazione dell'heap viene eseguita in C con malloc(). malloc() restituisce un puntatore a uno spazio appena assegnato, che viene quindi convertito in un puntatore, in qualcosa. Esempio:

int *i; 
i = (int *)malloc(sizeof(int)); 
*i=5; 

Mentre deallocazione di roba allocato sullo stack viene fatto automaticamente, deallocazione di roba allocato sul mucchio deve essere fatto dal programmatore.

La fonte della tua confusione deriva dal fatto che C# (che non uso, ma so che è simile a Java) non ha allocazione di stack. Quello che fai quando dici SomeClass sc, è di dichiarare un riferimento SomeClass che è attualmente non inizializzato fino a quando non dici new, che è il momento in cui l'oggetto sorge nell'esistenza. Prima dello new, non hai alcun oggetto. In C++ questo non è il caso. Non c'è alcun concetto di riferimento in C++ che sia simile a C# (o java), sebbene abbiate riferimenti in C++ solo durante chiamate di funzione (è un paradigma di pass-by-reference, in pratica. Di default C++ passa per valore, il che significa che voi copia oggetti alla chiamata di funzione). Tuttavia, questa non è l'intera storia. Controlla i commenti per dettagli più accurati.

+0

I confronti C#/C++ qui sono nel giusto spirito, ma non sono affatto accurati, su entrambi i lati. –

+0

@Merlyn Non so nulla di C#, e molto poco di java, quindi c'è una possibilità non trascurabile di essere stato impreciso o addirittura sbagliato –

+0

@Stefano: L'equivalente più vicino al comportamento automatico di C++ in C# è costituito da tipi di valore (structs e molti tipi built-in). Vengono copiati in base al valore e non è necessario chiamarli "nuovi". Se si chiama "new" su di essi, vengono comunque allocati nello spazio di memoria in cui si trovano (stack se in ambito locale, heap se in un oggetto allocato all'heap). –

2

Nel tuo caso, sc è allocato nello stack, utilizzando il costruttore predefinito per SomeClass. Poiché è in pila, l'istanza verrà distrutta al ritorno dalla funzione. (Questo sarebbe più impressionante se si istanziato SomeClass sc all'interno di una funzione chiamata da main --il memoria allocata per sc sarebbe non sono stati allocati al ritorno a main.)

Il new parola chiave, invece di allocare memoria sul run-time stack, alloca la memoria sull'heap. Dato che C++ non ha una garbage collection automatica, tu (il programmatore) sei responsabile di non allocare alcuna memoria allocata nell'heap (usando la parola chiave delete), per evitare perdite di memoria.

+0

Grazie. Quindi, se ho bisogno che il mio oggetto persista su più metodi non correlati (come un membro della classe?), Mi servirebbe di nuovo/cancella, mentre tutto in una funzione può andare in pila? –

+0

@ Michael: No. Basta fare: 'struct foo {int bar; }; ', e qualsiasi metodo' foo' potrebbe essere anche in grado di usare 'bar'. 'bar' ha la stessa durata di' foo', salva il costruttore. – GManNickG

+0

@Michael: puoi allocare sullo stack e poi fare in modo che la subroutine accetti un riferimento (che è un "alias"), invece di usare i puntatori, ma è molto poco pratico. –

2

Quando si dichiara una variabile (senza extern) in un ambito di funzione (ad esempio in main) è stata definita anche la variabile.La variabile entra in esistenza nel punto in cui la dichiarazione viene raggiunta e scompare quando viene raggiunta la fine del suo ambito (in questo caso la fine della funzione main).

Quando un oggetto viene creato, se ha un costruttore dichiarato dall'utente, viene utilizzato uno dei suoi costruttori per inizializzarlo. Analogamente, se ha un distruttore dichiarato dall'utente, questo viene utilizzato quando l'oggetto esce dall'ambito per eseguire eventuali azioni di pulizia richieste nel punto in cui esce dall'ambito. Questo è diverso dalle lingue che hanno finalizzatori che possono o meno eseguire e certamente non in un momento deterministico. È più simile a using/IDisposable.

Un'espressione new viene utilizzata in C++ per creare dinamicamente un oggetto. Solitamente viene utilizzato dove la durata di vita dell'oggetto non può essere associata a un particolare ambito. Ad esempio, quando deve continuare a esistere dopo aver completato la funzione che lo crea. Viene anche utilizzato dove il tipo esatto dell'oggetto da creare è ora noto al momento del compilatore, ad es. in una funzione di fabbrica. Gli oggetti creati dinamicamente possono spesso essere evitati in molti casi in cui sono comunemente usati in linguaggi come Java e C#.

Quando un oggetto viene creato con new, a un certo punto deve essere distrutto tramite un'espressione delete. Per assicurarsi che i programmatori non dimentichino di farlo, è normale utilizzare una sorta di oggetto puntatore intelligente per gestirlo automaticamente, ad es. a shared_ptr da tr1 o boost.

2

Alcune altre risposte stanno dicendo che "sc è allocato nello stack, nuovo alloca l'oggetto sull'heap". Preferisco non pensarci in questo modo perché confonde i dettagli di implementazione (stack/heap) con la semantica del codice. Dato che sei abituato al modo in cui C# fa le cose, penso che crei anche ambiguità. Invece, il modo in cui preferisco pensarci è il modo in cui lo standard C++ lo descrive:

sc è una variabile di tipo SomeClass, dichiarata a livello di blocco (ovvero, le parentesi che costituiscono la funzione principale). Questa è chiamata variabile locale . Perché non è dichiarato static o extern, questo lo ha ha durata di archiviazione automatica. Ciò significa che ogni volta che viene eseguita la riga SomeClass sc;, la variabile verrà inizializzata (eseguendo il suo costruttore) e quando la variabile uscirà dall'ambito uscendo dal blocco, verrà distrutta (eseguendo il suo distruttore - poiché si esegue non ne hai uno e il tuo oggetto è semplicemente vecchi dati, non sarà fatto nulla).

In precedenza ho detto "Perché non è dichiarato static o extern", se si fosse dichiarato come tale avrebbe durata archiviazione statica. Sarebbe inizializzato prima dell'avvio del programma (tecnicamente a livello di blocco sarebbe inizializzato al primo utilizzo) e distrutto dopo la chiusura del programma.

Quando si utilizza new per creare un oggetto, si crea un oggetto con la durata di archiviazione dinamica . Questo oggetto verrà inizializzato quando si chiama new e verrà distrutto solo se si chiama delete su di esso. Per chiamare delete, è necessario mantenere un riferimento ad esso e chiamare delete quando si finisce di usare l'oggetto. Generalmente, il codice C++ ben scritto non utilizza molto questo tipo di durata di archiviazione, ma in genere si collocano oggetti valore in contenitori (ad esempio std::vector), che gestiscono la durata dei valori contenuti. La variabile contenitore stessa può andare in memoria statica o archiviazione automatica.

Spero che questo aiuti a disambiguare le cose un po ', senza accumulare troppi termini nuovi per confondervi.

+2

Questo è semplicemente sbagliato. std :: vector non gestisce la durata degli oggetti. Sì, se spinga i membri allocati nello stack, sono legati all'ambito del vettore (perché li sta copiando), ma spingere i membri allocati nell'heap in un vettore std :: e aspettarsi che il vettore gestisca il loro ciclo di vita è solo sbagliato e consigli pericolosi da dare a qualcuno di nuovo in C++ – Falmarri

+0

@Falmarri Ti manca il punto, ma ho cercato di chiarire che gestisce i tipi di valore, non i tipi di riferimento. –

Problemi correlati