Я читаю статью Бьерна: Множественное наследование для 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* (чтобы стать его указателем this)». Компилятор преобразует вызов в:
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) Bjarne писал: При входе в 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
здесь!!!!!!
После приведенного выше фрагмента кода Бьярне продолжил писать: Обратите внимание, что указатель объекта, возможно, придется настроить так, чтобы он указывал на правильный подобъект, прежде чем искать элемент, указывающий на vtbl.
О чувак!!! Я совершенно не понимаю, что Бьерн пытался сказать? Можете ли вы помочь мне объяснить это?