detalhes deste ponteiro, função virtual e herança múltipla do compilador
Estou lendo o jornal de Bjarne:Herança Múltipla para C ++.
Na seção 3, página 370, Bjarne disse que "o compilador transforma uma chamada de uma função de membro em uma chamada de função" comum "com um argumento" extra "; esse argumento" extra "é um ponteiro para o objeto para o qual a função de membro é chamado."
Estou confuso com o extra deste argumento. Por favor, veja os dois exemplos a seguir:
Exemplo 1: (página 372)
class A {
int a;
virtual void f(int);
virtual void g(int);
virtual void h(int);
};
class B : A {int b; void g(int); };
class C : B {int c; void h(int); };
Um objeto de classe c C se parece com:
C:
----------- vtbl:
+0: vptr --------------> -----------
+4: a +0: A::f
+8: b +4: B::g
+12: c +8: C::h
----------- -----------
Uma chamada para uma função virtual é transformada em uma chamada indireta pelo compilador. Por exemplo,
C* pc;
pc->g(2)
torna-se algo como:
(*(pc->vptr[1]))(pc, 2)
O artigo de Bjarne me contou a conclusão acima. A passagemthis
O ponto é C *.
No exemplo a seguir, Bjarne contou outra história que me confundiu totalmente!
Exemplo 2: (página 373)
Dadas duas classes
class A {...};
class B {...};
class C: A, B {...};
Um objeto da classe C pode ser apresentado como um objeto contíguo como este:
pc--> -----------
A part
B:bf's this--> -----------
B part
-----------
C part
-----------
Chamar uma função de membro de B com um C *:
C* pc;
pc->bf(2); //assume that bf is a member of B and that C has no member named bf.
Bjarne escreveu: "Naturalmente, B :: bf () espera que um B * (se torne esse ponteiro)." O compilador transforma a chamada em:
bf__F1B((B*)((char*)pc+delta(B)), 2);
Por que aqui precisamos de um ponteiro B * para ser othis
? Se passarmos um ponteiro * C como othis
, ainda podemos acessar os membros de B corretamente, eu acho. Por exemplo, para obter o membro da classe B dentro de B :: bf (), basta fazer algo como: * (this + offset). esse deslocamento pode ser conhecido pelo compilador. Isto está certo?
Perguntas de acompanhamento para os exemplos 1 e 2:
(1) Quando é uma derivação de cadeia linear (exemplo 1), por que o objeto C pode estar no mesmo endereço que o subobjeto B e, por sua vez, A? Não há problema em usar um ponteiro C * para acessar os membros da classe B dentro da função B :: g no exemplo 1? Por exemplo, queremos acessar o membro b, o que acontecerá em tempo de execução? * (pc + 8)?
(2) Por que podemos usar o mesmo layout de memória (derivação de cadeia linear) para a herança múltipla? Supondo que no exemplo 2, classeA
, B
, C
tem exatamente os mesmos membros que o exemplo 1.A
: int a
ef
; B
: int b
ebf
(ou chame-og
);C
: int c
eh
. Por que não usar apenas o layout da memória como:
-----------
+0: a
+4: b
+8: c
-----------
(3) Eu escrevi um código simples para testar as diferenças entre a derivação da cadeia linear e a herança múltipla.
class A {...};
class B : A {...};
class C: B {...};
C* pc = new C();
B* pb = NULL;
pb = (B*)pc;
A* pa = NULL;
pa = (A*)pc;
cout << pc << pb << pa
Mostra quepa
, pb
epc
tem o mesmo endereço.
class A {...};
class B {...};
class C: A, B {...};
C* pc = new C();
B* pb = NULL;
pb = (B*)pc;
A* pa = NULL;
pa = (A*)pc;
Agora,pc
epa
tem o mesmo endereço, enquantopb
é algum deslocamento parapa
epc
.
Por que a compilação faz essas diferenças?
Exemplo 3: (página 377)
class A {virtual void f();};
class B {virtual void f(); virtual void g();};
class C: A, B {void f();};
A* pa = new C;
B* pb = new C;
C* pc = new C;
pa->f();
pb->f();
pc->f();
pc->g()
(1) A primeira pergunta é sobrepc->g()
relacionado à discussão no exemplo 2. A compilação faz a seguinte transformação:
pc->g() ==> g__F1B((*B)((char*)pc+delta(B)))
Ou temos que esperar o tempo de execução fazer isso?
(2) Bjarne escreveu: Ao entrar noC::f
, athis
ponteiro deve apontar para o início doC
objeto (e não para oB
parte). No entanto, não é geralmente conhecido em tempo de compilação que oB
apontado porpb
faz parte de umC
então o compilador não pode subtrair a constantedelta(B)
.
Por que não podemos conhecer oB
objeto apontado porpb
faz parte de umC
no momento da compilação? Com base no meu entendimento,B* pb = new C
, pb
aponta para um criadoC
objeto eC
herda deB
então umB
ponteiro pb aponta para parte doC
.
(3) Suponha que não sabemosB
ponteiro para porpb
faz parte de umC
no momento da compilação. Portanto, temos que armazenar o delta (B) para o tempo de execução que é realmente armazenado com o vtbl. Portanto, a entrada vtbl agora se parece com:
struct vtbl_entry {
void (*fct)();
int delta;
}
Bjarne escreveu:
pb->f() // call of C::f:
register vtbl_entry* vt = &pb->vtbl[index(f)];
(*vt->fct)((B*)((char*)pb+vt->delta)) //vt->delta is a negative number I guess
Estou totalmente confuso aqui. Por que (B *) não um (C *) em(*vt->fct)((B*)((char*)pb+vt->delta))
???? Com base no meu entendimento e na introdução de Bjarne na primeira frase da seção 5.1 e na página 377, devemos passar um C * comothis
aqui!!!!!!
Seguido pelo trecho de código acima, Bjarne continuou escrevendo: Observe que o ponteiro do objeto pode ter que ser ajustado para po int no subobjeto correto antes de procurar o membro que aponta para o vtbl.
Oh cara!!! Não tenho idéia do que Bjarne tentou dizer? Você pode me ajudar a explicar isso?