Рассуждая об этом, я помогаю сохранять четкое представление о расположении классов в памяти и, в частности, о том, что объект der
содержит подобъект base
, который имеетточно такой же макет памяти, как и у любого другого объекта base
.
В частности, ваш макет объекта base
будет просто содержать указатель на vtable (нет полей) и подобъект base
объекта der
также будет содержать этот указатель, отличается только значение, хранящееся в указателе, и он будет ссылаться на der
версию base
vtable (чтобы сделать его немного более интересным, учтите, что и base
, и der
содержит элементы):
// Base object // base vtable (ignoring type info)
+-------------+ +-----------+
| base::vptr |------> | &base::fn |
+-------------+ +-----------+
| base fields |
+-------------+
// Derived object // der vtable
+-------------+ +-----------+
| base::vptr |------> | &der::fn |
+-------------+ +-----------+
| base fields |
+-------------+ <----- [ base subobject ends here ]
| der fields |
+-------------+
Если вы посмотрите на два рисунка, вы можете распознать подобъект base
в объекте der
, когда вы делаете base *bp = &d;
, то, что вы делаете, получаетуказатель на base
подобъект внутри der
.В этом случае область памяти подобъекта base
точно такая же, как и у подобъекта base
, но это не обязательно должно быть так.Важно то, что указатель будет ссылаться на подобъект base
, а указанная память имеет макет памяти base
, но с той разницей, что указатели, хранящиеся в объекте, будут ссылаться на der
версии vtable.
Когда компилятор увидит код bp->fn()
, он будет считать его объектом base
и знает, где находится vptr в base
object, и он также знает, что fn
является первой записью в vtable, поэтому ему нужно только сгенерировать код для bp->vptr[ 0 ]()
.Если bp
относится к base
объекту, тогда bp->vptr
будет ссылаться на base
vtable , а bp->vptr[0]
будет base::fn
.Если указатель с другой стороны ссылается на объект der
, то bp->vptr
будет ссылаться на der
vtable, а bp->vptr[0]
будет ссылаться на der::fn
.
Обратите внимание, что во время компиляциисгенерированный код для обоих случаев абсолютно одинаков: bp->vptr[0]()
и что он отправляется различным функциям на основе данных, хранящихся в объекте base
(sub), в частности значения, хранящегося в vptr
, который получаетобновлено в конструкции.
Четко акцентируя внимание на том факте, что подобъект base
должен присутствовать и совместим с объектом base
, вы можете рассматривать более сложные сценарии как множественное наследование:
struct data {
int x;
};
class other : public data, public base {
int y;
public:
virtual void fn() {}
};
+-------------+
| data::x |
+-------------+ <----- [ base subobject starts here ]
| base::vptr |
+-------------+
| base fields |
+-------------+ <----- [ base subobject ends here ]
| other::y |
+-------------+
int main() {
other o;
base *bp = o;
}
Это более интересный случай, когда есть другая база, в этот момент вызов base * bp = o;
создает указатель на подобъект base
и может быть проверен, чтобы указывать на другое местоположение, чемобъект o
(попробуйте распечатать значения &o
и bp
).С вызывающего сайта это не имеет большого значения, потому что bp
имеет статический тип base*
, и компилятор всегда может разыменовать этот указатель, чтобы найти base::vptr
, используйте его, чтобы найти fn
в vtable и в итоге вызвать other::fn
.
В этом примере происходит немного больше магии, так как подобъекты other
и base
не выровнены перед вызовом действительной функции other::fn
, указателя this
должен быть скорректирован.Компилятор решает проблему, сохраняя не указатель на other::fn
в other
vtable , а скорее указатель на virtual thunk (небольшой фрагмент кода, который фиксирует значение this
и переадресация вызова на other::fn
)