Понимание указателей С++ для родительских/базовых классов

Мне задали этот вопрос, и я понял, что это неправильно. "Каков результат": мой ответ был 135, фактический результат - 136. Это означает, что указатель на два родительских класса не равен, даже если они проходят предыдущий тест, равный дочернему классу. Я думал, что понял С++ указатели, но это меня озадачило для объяснения. Хотя я думаю, что вижу, что происходит, я не знаю, почему. Любые эксперты С++, которые могут предложить техническое объяснение? Похоже, что первые два сравнения более логичны по своей природе, а последнее сравнение более буквально...

#include <iostream>

class A
{
    public:
    A() : m_i(0) { }

    protected:
    int m_i;
};

class B
{
    public:
    B() : m_d(0.0) { }

    protected:
    double m_d;
};

class C
    : public A, public B
{
    public:
    C() : m_c('a') { }

    private:
    char m_c;
};

int main()
{
    C c;
    A *pa = &c;
    B *pb = &c;

    const int x = (pa == &c) ? 1 : 2;
    const int y = (pb == &c) ? 3 : 4;
    const int z = (reinterpret_cast<char*>(pa) == reinterpret_cast<char*>(pb)) ? 5 : 6;

    std::cout << x << y << z << std::endl;
    return 0;
}

Ответы

Ответ 1

Возможно, если вы распечатаете pa и pb, будет более понятно, что происходит.

std::cout << "A: " << pa << std::endl;
std::cout << "B: " << pb << std::endl;
std::cout << "C: " << &c << std::endl;

Запуск этого в конце main дает мне

A: 0xbfef54e0
B: 0xbfef54e4
C: 0xbfef54e0

(ваш результат может быть другим, важно то, что они не все равны)

Это связано с тем, как объект C представлен в памяти. Так как a C является как A, так и B, ему необходимо иметь элементы данных из каждой части. Реальная компоновка C - это нечто подобное (игнорирование заполнения):

int    A::m_i;
double B::m_d;
char   C::m_c;

Когда вы конвертируете C* в A*, компилятор знает, что часть A начинается со смещения 0, поэтому значение указателя не изменяется. Для C* до B* его необходимо компенсировать с помощью sizeof(int) (плюс отступы). Эта обработка смещения выполняется автоматически для вас для расчета x и y. Для z он обходит, так как вы используете reinterpret_cast

Ответ 2

С++ обычно описывает структуры классов один за другим. Позволяет проверить следующий пример кода:

#include <iostream>
#include <stdio.h>

//#define DO_PACKSTRUCTURES
//#define DEFINE_VFUNC

#ifdef DO_PACKSTRUCTURES
#pragma pack(push, 1)
#endif


class A
{
public:
    A() : m_i(0) { }

#ifdef DEFINE_VFUNC
    virtual 
#endif    
    void myfunc()
    {
    }

protected:
    int m_i;
};

class B
{
public:
    B() : m_d(0.0) { }

#ifdef DEFINE_VFUNC
    virtual 
#endif    
    void myfunc2()
    {
    }

protected:
    double m_d;
};

class C
    : public A, public B
{
public:
    C() : m_c('a') { }

#ifdef DEFINE_VFUNC
    virtual
#endif    
    void myfunc3()
    {
    }

    char m_c;
};

#ifdef DO_PACKSTRUCTURES
#pragma pack(pop)
#endif

void pprint(char* prefix, void* p)
{
    printf("%s = %p\r\n", prefix, p);
}

int main()
{
    C c;
    A *pa = &c;
    B *pb = &c;

    pprint("&c", &c);
    pprint("pa", pa);
    printf("\r\n");

    pprint("pb", pb);
    pprint("pa + sizeof(A)", ((char*)pa) + sizeof(A));
    printf("\r\n");

    pprint("&c.m_c", &c.m_c);
    pprint("pb + sizeof(B)", ((char*)pb) + sizeof(B));
    printf("\r\n");
    printf("sizeof(A)=%d\r\n", sizeof(A));
    printf("sizeof(B)=%d\r\n", sizeof(B));
    printf("sizeof(C)=%d\r\n", sizeof(C));
    printf("sizeof(int)=%d\r\n", sizeof(int));
    printf("sizeof(double)=%d\r\n", sizeof(double));
    printf("sizeof(char)=%d\r\n", sizeof(char));
    printf("sizeof(void*)=%d\r\n", sizeof(void*));

    pa->myfunc();
    c.myfunc2();
    c.myfunc3();

    return 0;
}

Win32/Debug или Release/DO_PACKSTRUCTURES и DEFINE_VFUNC undefined:

&c = 00BBF7A4
pa = 00BBF7A4

pb = 00BBF7AC
pa + sizeof(A) = 00BBF7A8

& c.m_c = 00BBF7B4
pb + sizeof(B) = 00BBF7B4

sizeof(A) = 4
sizeof(B) = 8
sizeof(C) = 24
sizeof(int) = 4
sizeof(double) = 8
sizeof(char) = 1
sizeof(void*) = 4

Таким образом, указатель на C * является тем же самым указателем, что и A *, потому что A - это первый базовый класс, из которого начинается макет памяти. Итак, расположение памяти выглядит примерно так:

C*:
   A
   B
   C members (m_c)

pb не равен (pa + sizeof (A)), потому что компилятор добавляет несколько байт выравнивания между A и B для ускорения доступа к B. Не уверен, насколько важны эти оптимизации - создание миллионов экземпляров одного и того же класса может будет иметь влияние на производительность.

Определяется Win32/Debug или Release/DO_PACKSTRUCTURES, а DEFINE_VFUNC - undefined:

&c = 00B9F770
pa = 00B9F770

pb = 00B9F774
pa + sizeof(A) = 00B9F774

&c.m_c = 00B9F77C
pb + sizeof(B) = 00B9F77C

sizeof(A)=4
sizeof(B)=8
sizeof(C)=13
sizeof(int)=4
sizeof(double)=8
sizeof(char)=1
sizeof(void*)=4

Теперь мы не добавляем байты выравнивания или заполнения (из-за #pragma pack (push, 1)) - мы получили класс C меньше, а также теперь pb == (pa + sizeof (A)). Теперь мы также видим, что такое "выделение" класса C - sizeof (int)/sizeof (double) + sizeof (char) = 4 + 8 + 1 = 13.

Win32/Debug или Release/DO_PACKSTRUCTURES и DEFINE_VFUNC определены:

&c = 007EFCF4
pa = 007EFCF4

pb = 007EFCFC
pa + sizeof(A) = 007EFCFC

&c.m_c = 007EFD08
pb + sizeof(B) = 007EFD08

sizeof(A)=8
sizeof(B)=12
sizeof(C)=21
sizeof(int)=4
sizeof(double)=8
sizeof(char)=1
sizeof(void*)=4

У нас все еще есть совпадение с указателем, как в предыдущем случае, но если мы вычисляем размер с помощью sizeof (class) - мы получим правильный размер, но не sizeof (тип члена) - из-за "виртуального" ключевого слова - выделяя дополнительный размер указателя виртуальной таблицы.

 It reflects 21 - 8 - 4 - 1 = 8
 8 / sizeof(void*) = 2 - that two vtables - one from A and another from B class instances.

Я не уверен, почему сам класс C не имеет собственного vtable - для моего лучшего понимания он должен. Это можно понять позже. Компилятор Microsoft Visual С++ также имеет специальное ключевое слово __declspec (novtable), которое также имеет некоторое представление о том, как генерируется vtable. Но что-то нормальным разработчикам не нужно, если только вы не имеете дело с расширенным программированием COM.