Le classi sono (più o meno) costruite come strutture normali. I metodi sono (più o meno ...) convertiti in funzioni il cui primo parametro è "questo". I riferimenti alle variabili di classe vengono eseguiti come offset di "questo".
Per quanto riguarda l'ereditarietà, è possibile citare le domande frequenti in C++ LITE, che è speculare qui http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.4. Questo capitolo mostra come funzioni virtuali sono chiamati in hardware reale (quello che fa il compilazione fare in codice macchina
Lavoriamo un esempio Supponiamo Classe base ha 5 funzioni virtuali:.. virt0()
attraverso virt4()
.
// Your original C++ source code
class Base {
public:
virtual arbitrary_return_type virt0(...arbitrary params...);
virtual arbitrary_return_type virt1(...arbitrary params...);
virtual arbitrary_return_type virt2(...arbitrary params...);
virtual arbitrary_return_type virt3(...arbitrary params...);
virtual arbitrary_return_type virt4(...arbitrary params...);
...
};
Passo # 1: il compilatore costruisce una tabella statica che contiene 5 funzioni-puntatori, seppellendo quel tavolo nella memoria statica da qualche parte. Molti compilatori (non tutti) definiscono questa tabella durante la compilazione del file .cpp che definisce la prima funzione virtuale non in linea di Base. Chiamiamo la tabella v-table; facciamo finta che il suo nome tecnico sia Base::__vtable
. Se un puntatore di funzione si inserisce in una parola macchina sulla piattaforma hardware di destinazione, Base::__vtable
finirà col consumare 5 parole nascoste di memoria. Non 5 per istanza, non 5 per funzione; appena 5. Potrebbe sembrare qualcosa di simile al seguente pseudo-codice:
// Pseudo-code (not C++, not C) for a static table defined within file Base.cpp
// Pretend FunctionPtr is a generic pointer to a generic member function
// (Remember: this is pseudo-code, not C++ code)
FunctionPtr Base::__vtable[5] = {
&Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4
};
Passo # 2: il compilatore aggiunge un puntatore nascosta (di solito anche una macchina-word) a ciascun oggetto della classe base. Questo è chiamato il v-pointer. Pensate a questo puntatore nascosta come membro dati nascosti, come se il compilatore riscrive la classe a qualcosa di simile:
// Your original C++ source code
class Base {
public:
...
FunctionPtr* __vptr; ← supplied by the compiler, hidden from the programmer
...
};
Step # 3: il compilatore inizializza this->__vptr
all'interno di ogni costruttore. L'idea è quella di provocare v-pointer di ciascun oggetto per puntare a v-table della sua classe, come se si aggiunge la seguente istruzione in init-list di ogni costruttore:
Base::Base(...arbitrary params...)
: __vptr(&Base::__vtable[0]) ← supplied by the compiler, hidden from the programmer
...
{
...
}
Ora cerchiamo di lavorare fuori una classe derivata. Supponiamo che il tuo codice C++ definisca la classe Der che eredita dalla classe Base. Il compilatore ripete i passaggi 1 e 3 (ma non # 2). Al punto # 1, il compilatore crea una v-table nascosta, mantenendo gli stessi puntatori funzione come in Base::__vtable
ma sostituendo gli slot corrispondenti alle sostituzioni. Per esempio, se Der sovrascrive virt0()
attraverso virt2()
ed eredita gli altri come-si, v-table di Der potrebbe essere simile a questa (finta Der non aggiunge alcuna nuova pacchetti virtuali):
// Pseudo-code (not C++, not C) for a static table defined within file Der.cpp
// Pretend FunctionPtr is a generic pointer to a generic member function
// (Remember: this is pseudo-code, not C++ code)
FunctionPtr Der::__vtable[5] = {
&Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4
}; ^^^^----------^^^^---inherited as-is
Nel passo # 3, il compilatore aggiunge un'assegnazione puntatore simile all'inizio di ciascuno dei costruttori di Der. L'idea è di cambiare il v-pointer di ogni oggetto Der in modo che punti alla v-table della sua classe. (Questo non è un secondo puntatore v, è lo stesso v-pointer definito nella classe base, Base; ricorda, il compilatore non ripete il passaggio n. 2 nella classe Der.)
Infine, vediamo come il compilatore implementa una chiamata a una funzione virtuale. Il vostro codice potrebbe essere simile a questo:
// Your original C++ code
void mycode(Base* p)
{
p->virt3();
}
Il compilatore non ha idea se questo sta andando a chiamare Base::virt3()
o Der::virt3()
o forse il metodo di un'altra classe derivata che non ha nemmeno esiste ancora virt3()
. Si sa solo che si sta chiamando virt3()
, che capita di essere la funzione nello slot n. 3 della v-table. Si riscrive la chiamata in qualcosa di simile a questo:
// Pseudo-code that the compiler generates from your C++
void mycode(Base* p)
{
p->__vptr[3](p);
}
vi consiglio vivamente di ogni sviluppatore C++ per leggere le FAQ. Potrebbero essere necessarie diverse settimane (dato che è difficile da leggere e molto tempo), ma ti insegnerà molto sul C++ e su cosa può essere fatto con esso.
Apparirà più o meno come una struttura C. –
Giusto !. Quindi questo mi ha portato a pensare come sarebbero state compilate le strutture, e ho capito che forse non capisco questa parte, tanto per cominciare. bash $ cat struct.cpp struct test { int i; float f; }; Che cosa dovrebbe contenere il file corrispondente a questo file? Capisco, i file oggetto corrispondenti alle strutture non assomiglieranno a quelli delle funzioni nel senso che non ci sono istruzioni di assemblaggio. Grazie, – xyz
in particolare, questa struct.cpp non dovrebbe avere né un testo né una sezione di dati nei file oggetto? Non lo so, se questa domanda si sta piegando di più verso il formato dei file elf. – xyz