Что происходит внутри, когда мы делаем удручение?

Я пытался понять даункинг … Вот что я пытался …

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

Может кто-нибудь объяснить, что происходит внутри.

Спасибо…

2

Решение

Приведение в стиле 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 ++ Липпман. Это несколько старая книга, но она посвящена проблемам, которые должен решить компилятор, и некоторым решениям, которые использовали компиляторы.

7

Другие решения

поскольку display() не является виртуальным, вызов его не использует значение указателя в большинстве реализаций c ++. Итак, вы звоните display() через его статический адрес.
А так как display () не использует this, оно работает.

Однако, как отмечалось в комментариях, это все еще неопределенное поведение. Другой компилятор может привести к сбою.

Вы также можете позвонить display() из nullptr указатель, это даст те же результаты в большинстве реализаций. Но все еще неопределенное поведение.

2

По вопросам рекламы [email protected]