Сначала вы должны понять, что реальный объект создан, *o
имеет тип C12
- потому что это то, что вы создали с new C12()
.
Затем, с виртуальными функциями, будет вызван член для фактического объекта , независимо от типа, на который вы указываете указатель. Таким образом, когда вы приводите указатель I2
в I2 *o = new C12()
, для нижележащего объекта не имеет значения, если, например, вы затем вызываете o-> g (), так как объект «знает», чтобы вызвать егопереопределенная функция.
Однако, когда вы приводите указатель на «несвязанный» I1*
, вы попадаете в странное состояние. Принимая во внимание, что классы I1
и I2
, по сути, имеют идентичные макеты памяти, то вызов f()
в одном будет указывать на то же «смещение», что и вызов g()
в другом. Но, поскольку o
на самом деле является указателем на I2
, запись v-таблицы, которой заканчивается вызов, является смещением g
в I2, которое переопределяется на C12
.
ЭтоТакже следует отметить, что вы использовали приведение в стиле C, чтобы получить от I2*
до I1*
(но вы также можете использовать reinterpret_cast
). Это важно, потому что оба эти абсолютно ничего не делают с указателем или с указанным объектом / памятью.
Возможно, звучит немного искаженно, но янадеюсь, что это даст некоторое представление!
Вот возможный макет / сценарий памяти - но это будет зависеть от реализации и с использованием указателя класса после C-приведение стиля вполне может представлять собой неопределенное поведение!
Возможная карта памяти (упрощенная, при условии 4 байта для всех компонентов):
class I1:
0x0000: (non-virtual data for class I1)
0x0004: v-table entry for function "f"
class I2:
0x0000: (non-virtual data for class I2)
0x0004: v-table entry for function "g"
class C12:
0x0000: (non-virtual data for class I1)
0x0004: v-table entry for function "f"
0x0008: (non-virtual data for class I2)
0x000C: v-table entry for function "g"
0x0010: (class-specific stuff for C12)
Теперь, когда вы выполняете преобразование из C12*
до I2*
в I2 *o = new C12();
, компилятор понимает связь между двумя классами, поэтому o
будет указывать на смещение 0x0008
в C12 (производный класс был правильно «нарезан»). Но приведение в стиле C от I2*
до I1*
ничего не меняет, поэтому компилятор "думает", что он указывает на I1
, но он все еще указывает нафактический I2
кусочек C12
- и это «выглядит» как настоящий I1
класс.
домашнее задание
Что может показаться вам интересным (а может и нет)согласен с макетом памяти, который я описал), добавив следующий код к концу main()
:
C12* properC12 = new C12();// Points to the 'origin' of the class
I1* properI1 = properC12; // Should (?) have same value as above?
I2* properI2 = properC12; // Should (?) have an offset to 'slice'
I1* dodgyI1 = (I1*)properC12; // Will (?) have same value as properI2!
cout << std::hex << properC12 << endl;
cout << std::hex << properI1 << endl;
cout << std::hex << properI2 << endl;
cout << std::hex << dodgyI1 << endl;
Пожалуйста, - кто-нибудь, кто попытается - сообщите нам, какие значения и какая платформа/ компилятор, который вы используете. В Visual Studio 2019, компилируясь для платформы x64, я получаю следующие значения указателя:
000002688A9726E0
000002688A9726E0
000002688A9726E8
000002688A9726E0
..., что (вроде) соответствует макету памяти, который я описал (кроме того, что где-то есть v-таблицы)иначе, а не в блоке).