2009-03-15 9 views
17

Sto provando a capire che tipo di memoria viene colpita creando una vasta gamma di oggetti. So che ogni oggetto - una volta creato - avrà spazio nello HEAP per le variabili membro, e penso che tutto il codice per ogni funzione che appartiene a quel tipo di oggetto esista nel segmento di codice in memoria - permanentemente.In C++, dove sono messe in memoria le funzioni di classe?

È giusto?

Quindi, se posso creare 100 oggetti in C++, posso stimare che avrò bisogno di spazio per tutte le variabili membro quell'oggetto possiede moltiplicato per 100 (problemi possibili di allineamento qui), e poi ho bisogno di spazio nel segmento di codice per una singola copia del codice per ogni funzione membro per quel tipo di oggetto (non 100 copie del codice).

Le funzioni virtuali, il polimorfismo, il fattore ereditari in questo in qualche modo?

E gli oggetti provenienti da librerie collegate dinamicamente? Presumo che le DLL ottengano i propri segmenti stack, heap, codice e dati.

esempio semplice (potrebbe non essere sintatticamente corretto):

// parent class 
class Bar 
{ 
public: 
    Bar() {}; 
    ~Bar() {}; 

    // pure virtual function 
    virtual void doSomething() = 0; 

protected: 
    // a protected variable 
    int mProtectedVar; 
} 

// our object class that we'll create multiple instances of 
class Foo : public Bar 
{ 
public: 
    Foo() {}; 
    ~Foo() {}; 

    // implement pure virtual function 
    void doSomething()   { mPrivate = 0; } 

    // a couple public functions 
    int getPrivateVar()   { return mPrivate; } 
    void setPrivateVar(int v) { mPrivate = v; } 

    // a couple public variables 
    int mPublicVar; 
    char mPublicVar2; 

private: 
    // a couple private variables 
    int mPrivate; 
    char mPrivateVar2;   
} 

Circa la quantità di memoria dovrebbero 100 oggetti allocati dinamicamente di tipo Foo prendere compreso spazio per il codice e tutte le variabili?

risposta

26

Non è necessariamente vero che "ogni oggetto - una volta creato - avrà spazio nello HEAP per le variabili membro". Ogni oggetto creato creerà uno spazio diverso da zero da qualche parte per le sue variabili membro, ma da dove dipende l'allocazione dell'oggetto stesso. Se l'oggetto ha un'allocazione automatica (stack), lo saranno anche i suoi membri dei dati. Se l'oggetto è allocato sull'archivio gratuito (heap), lo saranno anche i suoi membri di dati. Dopotutto, qual è l'allocazione di un oggetto diverso da quello dei suoi membri dati?

Se un oggetto allocato allo stack contiene un puntatore o un altro tipo che viene quindi utilizzato per allocare nell'heap, quell'allocazione si verificherà nell'heap indipendentemente da dove è stato creato l'oggetto stesso.

Per gli oggetti con funzioni virtuali, ciascuno avrà un puntatore vtable assegnato come se fosse un membro di dati dichiarato esplicitamente all'interno della classe.

Come per le funzioni membro, il codice per quelle probabilmente non è diverso dal codice funzione libera in termini di dove va nell'immagine eseguibile. Dopo tutto, una funzione membro è fondamentalmente una funzione libera con un puntatore "questo" implicito come primo argomento.

L'ereditarietà non cambia molto di nulla.

Non sono sicuro di cosa intendi quando le DLL ottengono il proprio stack. Una DLL non è un programma e non dovrebbe avere bisogno di uno stack (o heap), poiché gli oggetti allocati sono sempre allocati nel contesto di un programma che ha il proprio stack e heap. Che ci sia un codice (testo) e segmenti di dati in una DLL ha senso, anche se io non sono esperto nell'implementazione di tali cose su Windows (che presumo tu stia usando data la tua terminologia).

+0

Presupposto "Se l'oggetto dispone di allocazione automatica (stack), anche i relativi membri dati" non sono corretti. Prendi un vettore std :: che è dichiarato come variabile stack. Se si inseriscono nuovi valori nel vettore, verranno allocati nell'heap, non nello stack – newgre

+5

Questo è il motivo per cui ho anche detto nella mia risposta "Se un oggetto allocato allo stack contiene un puntatore o un altro tipo che viene quindi utilizzato per allocare sull'heap, quell'allocazione si verificherà nell'heap indipendentemente da dove è stato creato l'oggetto stesso. " std :: vector è comunemente implementato come contenente tre puntatori. –

+0

hai ragione, ho frainteso il tuo post – newgre

2

Sebbene alcuni aspetti di questo dipendono dal compilatore. Tutto il codice compilato entra in una sezione di memoria nella maggior parte dei sistemi chiamati 'testo'. questo è separato dalle sezioni heap e stack (una quarta sezione, 'data', contiene la maggior parte delle costanti). L'istanziazione di molte istanze di una classe comporta lo spazio di runtime solo per le sue variabili di istanza, non per nessuna delle sue funzioni.Se utilizzi metodi virtuali, otterrai un ulteriore, ma piccolo, bit di memoria riservato alla tabella di ricerca virtuale (o equivalente per i compilatori che usano un altro concetto), ma la sua dimensione è determinata dal numero di metodi virtuali volte il numero di classi virtuali, ed è indipendente dal numero di istanze a run-time

Questo è vero per il codice staticamente e dinamicamente collegati. Il codice reale vive in una regione di "testo". La maggior parte dei sistemi operativi in ​​realtà può condividere il codice dll su più applicazioni, quindi se più applicazioni utilizzano le stesse DLL, solo una copia risiede in memoria e entrambe le applicazioni possono usarla. Ovviamente non ci sono risparmi aggiuntivi dalla memoria condivisa se solo un'applicazione utilizza il codice collegato.

+0

Si noti che "l'ereditarietà virtuale" ha un significato specifico diverso da quello che probabilmente si intende ("metodi virtuali"). –

+0

Hmm ... hai ragione ... modificato! – SingleNegationElimination

1

Non si può del tutto accurato dire la quantità di memoria di una classe o di oggetti X occuperanno nella RAM.

Tuttavia, per rispondere alle vostre domande, lei ha ragione esiste che il codice in un solo luogo, non è mai "assegnata". Il codice è quindi per classe, ed esiste se si creano o meno oggetti. La dimensione del codice è determinata dal compilatore, e anche allora i compilatori possono spesso dire di ottimizzare le dimensioni del codice, portando a risultati diversi.

funzioni virtuali non sono diversi, salvare il (piccolo) sovraccarico di un tavolo metodo virtuale, che di solito è per-class ha aggiunto.

Per quanto riguarda le DLL e altre librerie ... le regole non sono diversi a seconda di dove il codice è venuto da, quindi questo non è un fattore di utilizzo della memoria.

5

Il codice esiste nel segmento di testo e la quantità di codice generata in base alle classi è ragionevolmente complessa. Una classe noiosa senza ereditarietà virtuale ha apparentemente un codice per ogni funzione membro (compresi quelli che sono creati implicitamente se omessi, come i costruttori di copia) solo una volta nel segmento di testo. La dimensione di ogni istanza di classe è, come hai affermato, generalmente la dimensione della somma delle variabili membro.

Quindi, diventa piuttosto complesso. Alcuni dei problemi sono ...

  • Il compilatore può, se vuole o viene indicato, codice inline. Quindi, anche se potrebbe essere una funzione semplice, se è usata in molti posti e scelta per l'inlining, può essere generato molto codice (diffuso in tutto il codice del programma).
  • L'ereditarietà virtuale aumenta le dimensioni di ogni membro polimorfico. VTABLE (tabella virtuale) si nasconde insieme a ciascuna istanza di una classe utilizzando un metodo virtuale, contenente informazioni per la distribuzione runtime. Questa tabella può diventare abbastanza grande, se hai molte funzioni virtuali o più eredità (virtuale). Chiarimento: VTABLE è per classe, ma i puntatori al VTABLE sono memorizzati in ogni istanza (a seconda della struttura del tipo ancestrale dell'oggetto).
  • I modelli possono causare ingigantire il codice. Ogni utilizzo di una classe basata su modelli con un nuovo set di parametri del modello può generare un codice nuovo per ogni membro. I compilatori moderni cercano di comprimerlo il più possibile, ma è difficile.
  • L'allineamento/riempimento della struttura può far sì che le istanze di classi semplici siano più grandi di quanto previsto, poiché il compilatore esegue il rilievo della struttura per l'architettura di destinazione.

Durante la programmazione, utilizzare l'operatore sizeof per determinare la dimensione dell'oggetto - mai codice rigido. Utilizza la metrica approssimativa di "Somma della dimensione della variabile membro + un po 'VTABLE (se esiste)" quando valuti quanto costosi saranno i grandi gruppi di istanze e non preoccuparti eccessivamente delle dimensioni del codice. Ottimizza più tardi, e se uno qualsiasi dei problemi non ovvi tornerà a significare qualcosa, sarò piuttosto sorpreso.

+0

Adam: È necessario ricordare che l'overhead della dimensione VTable è sostenuto per un tipo * di * base, non per * oggetto *, sebbene la classe debba indicarlo (quindi, sì, quindi è necessario archiviare un puntatore per ogni classe, ma è così). – Arafangion

+0

Vero, lo chiarirò. Tuttavia, l'ereditarietà multipla e l'ereditarietà virtuale possono generare più di un puntatore VTABLE per istanza. –

+0

Grazie per aver aggiunto dettagli alle risposte a questa domanda! Risposta molto utile e ben organizzata. – petrocket

0

La stima è accurata nel caso base che hai presentato. Ogni oggetto ha anche un vtable con puntatori per ogni funzione virtuale, quindi aspettati un valore extra di memoria per ogni funzione virtuale.

Le variabili membro (e le funzioni virtuali) di qualsiasi classe base fanno anch'esse parte della classe, quindi includerle.

Proprio come in c è possibile utilizzare l'operatore sizeof (classname/datatype) per ottenere le dimensioni in byte di una classe.

0

Sì, è vero, il codice non viene duplicato quando viene creata un'istanza dell'oggetto. Per quanto riguarda le funzioni virtuali, la chiamata della funzione corretta viene determinata utilizzando vtable, ma ciò non influisce sulla creazione dell'oggetto di per sé.

Le DLL (librerie condivise/dinamiche in generale) sono mappate in memoria nello spazio di memoria del processo. Ogni modifica viene eseguita come Copy-On-Write (COW): una singola DLL viene caricata una sola volta nella memoria e per ogni scrittura in uno spazio mutabile viene creata una copia di tale spazio (generalmente di dimensioni pagina).

1

se compilato come 32 bit. quindi sizeof (Bar) dovrebbe dare 4. Foo dovrebbe aggiungere 10 byte (2 inte + 2 caratteri).

Poiché Foo è ereditato da Bar. Questo è almeno 4 + 10 byte = 14 byte.

GCC ha attributi per il riempimento delle strutture in modo che non ci sia riempimento. In questo caso 100 voci richiederebbero 1400 byte + un piccolo overhead per allineare l'allocazione + un po 'di overhead per la gestione della memoria.

Se non è specificato alcun attributo impaccato, dipende dall'allineamento dei compilatori.

Ma questo non considera la quantità di memoria che vtable occupa e la dimensione del codice compilato.

+0

Grazie per aver fatto i conti! Molto utile anche la prospettiva di gcc. – petrocket

0

E 'molto difficile dare una risposta esatta a yoour domanda, in quanto questo è implementtaion dipendente, ma i valori approssimativi per un'implementazione a 32 bit potrebbe essere:

int Bar::mProtectedVar; // 4 bytes 
int Foo::mPublicVar;  // 4 bytes 
char Foo::mPublicVar2;  // 1 byte 

ci sono problemi allgnment qui e il totale finale potrebbe essere 12 byte. Avrai anche un vptr - say anoter 4 bytes. Quindi la dimensione totale per i dati è di circa 16 byte per istanza. È impossibile dire quanto spazio occuperà il codice, ma hai ragione nel pensare che c'è solo una copia del codice condivisa tra tutte le istanze.

Quando si chiede

presumo DLL ottengono segmenti propria pila, heap, codice e dati.

La risposta è che non c'è davvero molta differenza tra i dati in una DLL e i dati in un'app - fondamentalmente condividono tutto tra loro, Questo deve essere così quando ci pensi su di esso - se avessero stack diversi (ad esempio) come funzionano le chiamate di funzione?

+0

Grazie per aver mostrato gli importi della memoria (su un PC tipico) e per chiarire il problema relativo alla DLL. – petrocket

1

Le informazioni fornite sopra sono di grande aiuto e mi hanno fornito alcune informazioni sulla struttura della memoria C++. Ma vorrei aggiungere che non importa quante funzioni virtuali in una classe, ci sarà sempre solo 1 VPTR e 1 VTABLE per classe. Dopo tutto il VPTR punta al VTABLE, quindi non c'è bisogno di più di un VPTR in caso di più funzioni virtuali.