подробности компилятора об этом указателе, виртуальной функции и множественном наследовании
Я читаю статью Бьярне:Множественное наследование для C ++.
В разделе 3 на странице 370 Бьярне сказал, что «компилятор превращает вызов функции-члена в« обычный »вызов функции с« дополнительным »аргументом; этот« дополнительный »аргумент является указателем на объект, для которого функция-член называется."
Я смущен дополнительным этим аргументом. Пожалуйста, посмотрите следующие два примера:
Пример 1: (стр. 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); };
Объект C класса C выглядит так:
C:
----------- vtbl:
+0: vptr --------------> -----------
+4: a +0: A::f
+8: b +4: B::g
+12: c +8: C::h
----------- -----------
Вызов виртуальной функции преобразуется компилятором в косвенный вызов. Например,
C* pc;
pc->g(2)
становится что-то вроде:
(*(pc->vptr[1]))(pc, 2)
Бумага Бьярне сообщила мне вышеупомянутое заключение. Прохождениеthis
Дело в том, C *.
В следующем примере Бьярн рассказал другую историю, которая полностью смутила меня!
Пример 2: (страница 373)
Дано два класса
class A {...};
class B {...};
class C: A, B {...};
Объект класса C может быть размечен как непрерывный объект, подобный этому:
pc--> -----------
A part
B:bf's this--> -----------
B part
-----------
C part
-----------
Вызов функции-члена B с заданным C *:
C* pc;
pc->bf(2); //assume that bf is a member of B and that C has no member named bf.
Бьярне писал: «Естественно, B :: bf () ожидает, что B * (станет его указателем)». Компилятор преобразует вызов в:
bf__F1B((B*)((char*)pc+delta(B)), 2);
Почему здесь нам нужен указатель B *, чтобы бытьthis
? Если мы просто передадим указатель * C в качествеthis
Я думаю, мы все еще можем получить доступ к членам B правильно. Например, чтобы получить член класса B внутри B :: bf (), нам просто нужно сделать что-то вроде: * (this + offset). это смещение может быть известно компилятору. Это правильно?
Последующие вопросы для примера 1 и 2:
(1) Когда это линейный деривационный цепочка (пример 1), почему можно ожидать, что объект C будет иметь тот же адрес, что и B, и, в свою очередь, подобъекты A? Нет проблем с использованием указателя C * для доступа к членам класса B внутри функции B :: g в примере 1? Например, мы хотим получить доступ к члену b, что произойдет во время выполнения? * (Рс + 8)?
(2) Почему мы можем использовать один и тот же макет памяти (линейная цепочка) для множественного наследования? Предполагая в примере 2, классA
, B
, C
имеют точно такие же члены, как в примере 1.A
: int a
а такжеf
; B
: int b
а такжеbf
(или назовите этоg
);C
: int c
а такжеh
, Почему бы просто не использовать макет памяти, как:
-----------
+0: a
+4: b
+8: c
-----------
(3) Я написал простой код для проверки различий между производным линейной цепочки и множественным наследованием.
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
Это показывает, чтоpa
, pb
а такжеpc
иметь тот же адрес.
class A {...};
class B {...};
class C: A, B {...};
C* pc = new C();
B* pb = NULL;
pb = (B*)pc;
A* pa = NULL;
pa = (A*)pc;
Сейчас,pc
а такжеpa
иметь тот же адрес, в то время какpb
какое-то смещение кpa
а такжеpc
.
Почему компиляция делает эти различия?
Пример 3: (страница 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) Первый вопрос оpc->g()
что относится к обсуждению в примере 2. Выполняет ли компиляция следующее преобразование:
pc->g() ==> g__F1B((*B)((char*)pc+delta(B)))
Или мы должны ждать выполнения для этого?
(2) Бьярне писал: При входе вC::f
,this
указатель должен указывать на началоC
объект (а не кB
часть). Однако, как правило, во время компиляции не известно, чтоB
указал наpb
является частьюC
поэтому компилятор не может вычесть константуdelta(B)
.
Почему мы не можем знатьB
объект, на который указываетpb
является частьюC
во время компиляции? Исходя из моего понимания,B* pb = new C
, pb
указывает на созданныйC
объект иC
наследует отB
такB
указатель pb указывает на частьC
.
(3) Предположим, что мы не знаемB
указатель наpb
является частьюC
во время компиляции. Таким образом, мы должны хранить дельту (B) для среды выполнения, которая фактически сохраняется с помощью vtbl. Итак, запись vtbl теперь выглядит так:
struct vtbl_entry {
void (*fct)();
int delta;
}
Бьярне написал:
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
Я полностью запутался здесь. Почему (B *) не (C *) в(*vt->fct)((B*)((char*)pb+vt->delta))
???? Основываясь на моем понимании и введении Бьярне в первом предложении в разделе 5.1 на 377 странице, мы должны передать C * какthis
Вот!!!!!!
Вслед за приведенным выше фрагментом кода Бьярне продолжил писать: обратите внимание, что указатель объекта, возможно, придется настроить для указания int на правильный подобъект, прежде чем искать элемент, указывающий на vtbl.
О чувак!!! Я совершенно не представляю, что Бьярн пытался сказать? Можете ли вы помочь мне объяснить это?