Значения указателя различны, но они сравниваются равными. Зачем?

Краткий пример выводит странный результат!

#include <iostream>

using namespace std;

struct A { int a; };    
struct B { int b; };
struct C : A, B
{
    int c;
};

int main()
{
    C* c = new C;
    B* b = c;

    cout << "The address of b is 0x" << hex << b << endl;
    cout << "The address of c is 0x" << hex << c << endl;

    if (b == c)
    {
        cout << "b is equal to c" << endl;
    }
    else
    {
        cout << "b is not equal to c" << endl;
    }
}

Мне очень удивительно, что вывод должен быть следующим:

The address of b is 0x003E9A9C
The address of c is 0x003E9A98
b is equal to c

Что заставляет меня задаться вопросом:

0x003E9A9C не равен 0x003E9A98, но выход "b равен c"

Ответ 1

Объект A C содержит два под-объекта типа A и B. Очевидно, что они должны иметь разные адреса, поскольку два отдельных объекта не могут иметь один и тот же адрес; поэтому максимум один из них может иметь тот же адрес, что и объект C. Вот почему печать указателей дает разные значения.

Сравнение указателей не просто сравнивает их числовые значения. Можно сравнивать только указатели того же типа, поэтому сначала нужно преобразовать их в соответствие друг с другом. В этом случае C преобразуется в B*. Это то же самое преобразование, которое используется для инициализации B в первую очередь: оно корректирует значение указателя так, чтобы оно указывало на под-объект B, а не на объект C, и два указателя теперь сравниваются равными.

Ответ 2

Макет памяти объекта типа C будет выглядеть примерно так:

|   <---- C ---->   |
|-A: a-|-B: b-|- c -|
0      4      8     12

Я добавил смещение в байтах из Адреса объекта (на платформе, подобной вашей, с sizeof (int) = 4).

В основном, у вас есть два указателя, я буду переименовывать их в pb и pc для ясности. pc указывает на начало всего объекта C, а pb указывает на начало подобъекта B:

   |   <---- C ---->   |
   |-A: a-|-B: b-|- c -|
   0      4      8     12
pc-^   pb-^

Вот почему их значения различны. 3E9A98 + 4 - 3E9A9C, в гексагоне.

Если вы сравните эти два указателя, компилятор увидит сравнение между B* и a C*, которые являются разными типами. Поэтому он должен применять неявное преобразование, если оно есть. pb не может быть преобразован в C*, но возможен обратный путь - он преобразует pc в B*. Это преобразование даст указатель, указывающий на подобъект B, где указывает pc, - это то же самое неявное преобразование, которое используется при определении B* pb = pc;. Результат равен pb, очевидно:

   |   <---- C ---->   |
   |-A: a-|-B: b-|- c -|
   0      4      8     12
pc-^   pb-^
   (B*)pc-^

Поэтому при сравнении двух указателей компилятор фактически сравнивает преобразованные указатели, равные.

Ответ 3

Я знаю, что есть ответ, но, возможно, это будет более простым и подкрепленным примером.

Существует неявное преобразование из C* в B* в операнд c здесь if (b == c)

Если вы перейдете с этим кодом:

#include <iostream>

using namespace std;

struct A { int a; };    
struct B { int b; };
struct C : A, B
{
    int c;
};

int main()
{
    C* c = new C;
    B* b = c;

    cout << "The address of b is 0x" << hex << b << endl;
    cout << "The address of c is 0x" << hex << c << endl;
    cout << "The address of (B*)c is 0x" << hex << (B*)c << endl;

    if (b == c)
    {
        cout << "b is equal to c" << endl;
    }
    else
    {
        cout << "b is not equal to c" << endl;
    }
}

Вы получаете:

The address of b is 0x0x88f900c
The address of c is 0x0x88f9008
The address of (B*)c is 0x0x88f900c
b is equal to c

Итак c, отбрасываемый в тип B*, имеет тот же адрес, что и b. Как и ожидалось.

Ответ 4

Если я могу добавить к Майку отличный ответ, если вы добавите их как void*, то вы получите ожидаемое поведение:

if ((void*)(b) == (void*)(c))
    ^^^^^^^       ^^^^^^^

печатает

b is not equal to c

Выполнение чего-то подобного на C (язык) на самом деле раздражало компилятор из-за сравнения различных типов указателей.

Я получил:

warning: comparison of distinct pointer types lacks a cast [enabled by default]

Ответ 5

В вычислениях (или, скорее, мы должны сказать в математике) может быть много понятий равенства. Любое соотношение, которое является симметричным, рефлексивным и транзитивным, может быть использовано как равенство.

В вашей программе вы изучаете два несколько разных понятия равенства: побитовое идентичность реализации (два указателя находятся на одном и том же адресе) по сравнению с другим видом равенства, основанным на идентичности объекта, который допускает два представления на одном и том же объекте, через ссылки на разные статические типы, которые должны рассматриваться как ссылки на один и тот же объект.

В этих по-разному типизированных представлениях используются указатели, которые не имеют одинакового значения адреса, потому что они фиксируются на разных частях объекта. Компилятор знает об этом и поэтому генерирует правильный код для сравнения равенства, который учитывает это смещение.

Это структура объектов, вызванных наследованием, что делает необходимым наличие этих смещений. Когда есть несколько оснований (благодаря множественному наследованию), только один из этих оснований может быть на низком адресе объекта, так что указатель на базовую часть совпадает с указателем на производный объект. Другие базовые части находятся в другом месте объекта.

Таким образом, наивное, побитовое сравнение указателей не даст правильных результатов в соответствии с объектно-ориентированным представлением объекта.

Ответ 6

Некоторые хорошие ответы здесь, но там короткая версия. "Два объекта одинаковы" не означает, что они имеют одинаковый адрес. Это означает, что в них помещаются данные, а данные из них эквивалентны.