Понимание виртуальных функций при получении из нескольких классов - PullRequest
3 голосов
/ 08 октября 2019

Я начал понимать работу виртуальных функций в C++ и наткнулся на следующий код. Вот мое понимание виртуальных функций:

  1. Каждый класс, который определяет виртуальную функцию, имеет для нее vtable .
  2. Когда экземпляркласс создан, vptr создан, что указывает на vtable этого класса.

Исходя из моего понимания, я пытаюсь проанализировать вывод следующего кода, который я не могу расшифровать, как код печатает «C12g».

class I1 {
  public: 
    virtual void f(){cout << "I1" << endl;}
};
class I2 {
  public: 
    virtual void g(){cout << "I2" << endl;}
};
class C12 : public I1, public I2 {
  public:
    virtual void f(){cout << "C12f" << endl;}
    virtual void g(){cout << "C12g" << endl;}
};
int main(int argc, char *argv[]) {
  I2 *o = new C12();
  ((I1*)o)->f();
}

Я думал, что, поскольку объект C12 назначен типу I2, объект o может получить доступ толькоего метод g() в C12 (поскольку g переопределяется). Теперь, так как o приведен к типу I1, я думал, что будет вызван f() в C12.

Фактический результат: C12g

Я хотел бы знатьо следующих вещах:

  1. Структура памяти C12 и что I1, I2 указывают на
  2. Как C12g печатается как вывод.
  3. Что происходит, когда объектприведение типа между двумя несвязанными интерфейсами?

1 Ответ

4 голосов
/ 08 октября 2019

Сначала вы должны понять, что реальный объект создан, *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-таблицы)иначе, а не в блоке).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...