Я пытался понять даункинг … Вот что я пытался …
class Shape
{
public:
Shape() {}
virtual ~Shape() {}
virtual void draw(void) { cout << "Shape: Draw Method" << endl; }
};
class Circle : public Shape
{
public:
Circle(){}
~Circle(){}
void draw(void) { cout << "Circle: Draw Method" << endl; }
void display(void) { cout << "Circle: Only CIRCLE has this" << endl; }
};
int main(void)
{
Shape newShape;
Circle *ptrCircle1 = (Circle *)&newShape;
ptrCircle1->draw();
ptrCircle1->display();
return EXIT_SUCCESS;
}
Здесь я опущен вниз путем преобразования назначения указателя базового класса в производный класс. То, что я понял, это …
Circle* ptrCircle1 --> +------+ new Shape()
|draw()|
+------+
Базовый класс не имеет информации о display()
метод, который есть в производном вызове. Я ожидал сбой, но он напечатал вывод как
Shape: Draw Method
Circle: Only CIRCLE has this
Может кто-нибудь объяснить, что происходит внутри.
Спасибо…
Приведение в стиле C, в данном случае и из-за отношения наследования, эквивалентно static_cast
, Как с большинством приведений (за исключением dynamic_cast
где некоторые проверки вводятся), когда вы говорите, что объект действительно Circle
компилятор будет доверять вам и предполагать, что это так. Поведение не определено в этом случае, как объект не Circle
, вы лжете компилятору и все ставки сняты.
Здесь действительно происходит то, что компилятор выясняет, есть ли смещение от базового к производному типу для этой комбинации, и соответствующим образом корректирует указатель. В этот момент вы получаете указатель на производный тип, который имеет скорректированный адрес, и безопасность типа отключена. Любой доступ через этот указатель будет предполагать, что память в этом месте — это то, что вы сказали, и будет интерпретировать это как таковое, что является неопределенным поведением, так как вы читаете память так, как если бы она имела тип, которым она не является.
Когда указатель настроен?
struct base1 { int x; };
struct base2 { int y; };
struct derived : base1, base2 {};
base2 *p = new derived;
Адрес derived
, base1
а также base1::x
то же самое, но отличается от адреса base2
а также base2::y
, Если вы кастовали из derived
в base2
компилятор настроит указатель в преобразовании (добавив sizeof(base1)
по адресу), при кастинге с base2
в derived
, компилятор будет корректироваться в противоположном направлении.
Почему вы получаете результаты, которые вы получаете?
Форма: метод рисования
Круг: это есть только в CIRCLE
Это связано с тем, как компилятор реализует динамическую диспетчеризацию. Для каждого типа с хотя бы одной виртуальной функцией компилятор будет генерировать одну (или несколько) виртуальных таблиц. Виртуальная таблица содержит указатели на окончательный переопределитель для каждой функции в типе. Каждый объект содержит указатель (и) на виртуальную таблицу (и) для полного типа. При вызове виртуальной функции компилятор выполняет поиск в таблице и следит за указателем.
В этом случае объект действительно Shape
, vptr будет ссылаться на виртуальную таблицу для Shape
, Когда вы разыгрываете из Shape
в Derived
вы говорите компилятору, что это Circle
(даже если это не так). Когда вы звоните draw()
компилятор следует vptr (в этом случае vptr для Shape
подобъект и Circle
подобъект оказывается в том же смещении (0 в большинстве ABI) от начала объекта. Вызов, введенный компилятором, следует за Shape
вптр (актерский состав делает не изменить любое содержимое памяти, что vptr по-прежнему Shape
) и ударил Shape::draw
,
В случае display()
вызов не отправляется динамически через vptr, поскольку он не является виртуальной функцией. Это означает, что компилятор будет вводить прямой вызов Circle::draw()
передавая адрес, который вы имеете в качестве this
указатель. Вы можете смоделировать это для виртуальной функции, отключив динамическую диспетчеризацию:
ptrCircle1->Circle::draw();
Помните, что это просто объяснение деталей компилятора, которые избегают стандарта C ++, по стандарту это просто Неопределенное поведение, что бы ни делал компилятор, это нормально. Другой компилятор может сделать что-то другое (хотя все ABI, которые я видел, здесь делают в основном то же самое).
Если вы действительно заинтересованы в деталях, как эти вещи работают, вы можете взглянуть на Внутри объектной модели C ++ Липпман. Это несколько старая книга, но она посвящена проблемам, которые должен решить компилятор, и некоторым решениям, которые использовали компиляторы.
поскольку display()
не является виртуальным, вызов его не использует значение указателя в большинстве реализаций c ++. Итак, вы звоните display()
через его статический адрес.
А так как display () не использует this
, оно работает.
Однако, как отмечалось в комментариях, это все еще неопределенное поведение. Другой компилятор может привести к сбою.
Вы также можете позвонить display()
из nullptr
указатель, это даст те же результаты в большинстве реализаций. Но все еще неопределенное поведение.