Почему указатель производного класса не может указывать на объект базового класса без приведения? - PullRequest
7 голосов
/ 17 марта 2012

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

Предположим, у нас есть следующая структура класса

class Pet {};
class Dog : public Pet {};

тогда следующее утверждение

a (Dog) is a (Pet)

может быть правдой в реальной жизни, , но НЕ верен в C ++ , по моему мнению. Просто посмотрите на логическое представление объекта Dog, это выглядит так:

enter image description here

Правильнее будет сказать

a (Dog) has a (Pet)

или

a (Pet) is a subset of (Dog)

, который, если вы заметили, является логической противоположностью слова "собака - это животное"


Теперь проблема в том, что # 1 ниже разрешен, в то время как # 2 нет:

Pet* p = new Dog;  // [1] - allowed!
Dog* d = new Pet;  // [2] - not allowed without explicit casting!

Насколько я понимаю, [1] не должно быть разрешено без предупреждений, потому что нет никакого способа, которым указатель должен указывать на объект типа его надмножества (объект Dog является надмножеством Pet) просто потому, что Pet не знает что-нибудь о новых членах, которые Dog мог объявить (подмножество Dog - Pet на диаграмме выше).

[1] эквивалентен int*, пытающемуся указать на double объект!

Совершенно очевидно, что здесь я упускаю ключевой момент, который перевернул бы все мои рассуждения с ног на голову. Подскажите, пожалуйста, что это?

Я считаю, что параллели с примерами из реального мира только усложняют ситуацию. Я бы предпочел понять это с точки зрения технических деталей. Спасибо!

Ответы [ 11 ]

29 голосов
/ 17 марта 2012

Редактировать: перечитывание вашего вопроса и мой ответ заставляют меня сказать это вверху:

Ваше понимание is a в C ++ (полиморфизм в целом) неверно.

A is B означает A has at least the properties of B, possibly more, по определению .

Это совместимо с вашими утверждениями о том, что Dog имеет Pet и что [атрибуты] домашнего питомца [являются] подмножеством [атрибутов] Dog.


Это вопрос определения полиморфизма и наследования. Рисованные вами диаграммы соответствуют представлению в памяти экземпляров Pet и Dog, но вводят в заблуждение при их интерпретации.

Pet* p = new Dog;

Указатель p определен так, чтобы указывать на любой Pet-совместимый объект , который в C ++ является любым подтипом Pet (Примечание: Pet сам по себе является подтипом по определению) , Среда выполнения гарантируется, что при доступе к объекту за p он будет содержать все, что ожидается от Pet, и, возможно, больше . Часть «возможно больше» - это Dog на вашей диаграмме. То, как вы рисуете свою диаграмму, вводит в заблуждение.

Подумайте о расположении членов класса в памяти:

Pet: [pet data]
Dog: [pet data][dog data]
Cat: [pet data][cat data]

Теперь, когда Pet *p указывает на, требуется наличие части [pet data] и, при необходимости, чего-либо еще. Из приведенного выше списка Pet *p может указывать на любой из трех. Пока вы используете Pet *p для доступа к объектам, вы можете получить доступ только к [pet data], потому что вы не знаете, что, во всяком случае, потом . Это контракт, который гласит Это, по крайней мере, домашнее животное, может быть, больше .

На все, на что указывает Dog *d, должны быть [pet data] и [dog data]. Таким образом, единственный объект в памяти, на который он может указывать, это собака. И наоборот, через Dog *d вы можете получить доступ как к [pet data], так и к [dog data]. Аналогично для Cat.


Давайте интерпретируем заявления, которые вас смущают:

Pet* p = new Dog;  // [1] - allowed!
Dog* d = new Pet;  // [2] - not allowed without explicit casting!

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

Указатель p ожидает найти [pet data] в том месте, на которое он указывает. Так как правая сторона - это Dog, и каждый Dog объект имеет [pet data] перед [dog data], указывать на объект типа Dog вполне нормально.

Компилятор не знает, что за указателем стоит else , и поэтому вы не можете получить доступ от [dog data] до p.

Объявление позволено , поскольку компилятор может гарантировать наличие [pet data] во время компиляции. (это утверждение явно упрощено от реальности, чтобы соответствовать вашему описанию проблемы)

1 эквивалентно тому, что int * пытается указать на двойной объект!

Между типами int и double нет такого подтипа, как между Dog и Pet в C ++ . Постарайтесь не смешивать их в обсуждении, потому что они разные: вы приводите между значениями от int и double ((int) double явно, (double) int неявно), вы не можете приводить между указателями им . Просто забудь это сравнение.

Что касается [2]: объявление гласит: «d указывает на объект, который имеет [pet data] и [dog data], возможно, больше». Но вы выделяете только [pet data], поэтому компилятор сообщает, что вы не можете этого сделать.

Фактически, компилятор не может гарантировать, что это нормально, и он отказывается от компиляции. Существуют допустимые ситуации, когда компилятор отказывается компилировать, но вы, программист, знаете лучше. Вот для чего static_cast и dynamic_cast. Простейший пример в нашем контексте:

d = p; // won't compile
d = static_cast<Dog *>(p); // [3]
d = dynamic_cast<Dog *>(p); // [4]

[3] всегда будет успешным и приведет к ошибкам, которые трудно отследить, если p на самом деле не Dog.
[4] вернет NULL, если p на самом деле не Dog.

Я настоятельно рекомендую попробовать эти броски, чтобы увидеть, что вы получите. Вы должны получить мусор для [dog data] из static_cast и указатель NULL для dynamic_cast, предполагая, что RTTI включен.

5 голосов
/ 17 марта 2012

С точки зрения технических деталей:

Дополнительная информация об объекте Dog добавляется в конец объекта Pet, поэтому префикс [в битах] Dog на самом деле Pet, поэтому проблем с назначением Dog* объект для Pet* переменной. Совершенно безопасно получить доступ к любому из Pet полей / методов объекта Dog.

Однако - опозит не соответствует действительности. Если вы назначите адрес Pet* переменной Dog*, а затем получите доступ к одному из полей Dog [которого нет в Pet], вы освободитесь из выделенного пространства.

Печатая рассуждения:
Также обратите внимание, что значение может быть присвоено переменной только в том случае, если оно имеет правильный тип без приведения [c ++ - это статическая типизация langauge]. Так как Dog - это Pet, Dog* - это Pet* - так что это не противоречит, но обратное неверно.

4 голосов
/ 17 марта 2012

Это вопрос иерархической классификации.Если я скажу своим детям, что у них может быть домашнее животное, то собаке, безусловно, разрешат.Но если я скажу им, что у них может быть только кошка, они не смогут попросить рыбу.

4 голосов
/ 17 марта 2012

Собака является домашним животным, потому что оно происходит от класса домашних животных.В C ++ это в значительной степени соответствует требованию ООП. Что такое принцип подстановки Лискова

Dog* d = new Pet;  // [2] - not allowed without explicit casting!

Конечно, это не разрешено, домашнее животное также может быть кошкой или попугаем.

2 голосов
/ 17 марта 2012

Я думаю, что вы путаете то, что - это , что должно означать в контексте ОО. Вы можете сказать, что Dog имеет субобъект Pet, и это верно, если вы переходите к битовому представлению объектов. Но важно то, что программирование - это моделирование реальности в программу, которую может обрабатывать компьютер. Наследование - это способ, которым вы моделируете отношения is-a , в соответствии с вашим примером:

A Dog is-a Pet

На обычном языке это означает, что он демонстрирует все виды поведения домашнего животного, возможно, с некоторыми характерными поведениями, которые различаются (лает), но это животное, и оно предоставляет компанию, которую вы можете кормить ... Все это поведение будет определяться (виртуальные) функции-члены в классе Pet и могут быть переопределены в типе Dog, так как определены другие операции. Но важная часть заключается в том, что при использовании наследования все экземпляры Dog могут использоваться там, где требуется интенсивность Pet.

1 голос
/ 17 марта 2012

Вы запутались между базовым и родительским классами.

Pet - базовый класс. Pet* может указывать любое количество различных типов, если они наследуются от Pet. Поэтому неудивительно, что Pet* pet = new Dog разрешено. Dog - это Pet. У меня есть указатель на Pet, который оказывается Dog.

С другой стороны, если у меня есть Pet*, я понятия не имею, на что он на самом деле указывает. Это может указывать на Dog, но также может указывать на Cat, Fish или что-то еще целиком. Таким образом, язык не позволяет мне звонить Pet->bark(), потому что не все Pet s могут bark() (например, Cat s meow()).

Если, однако, у меня есть Pet*, который, как я знаю, , фактически, Dog, тогда совершенно безопасно привести к Dog, а затем вызвать bark() .

Итак:

Pet* p = new Dog;  // sure this is allowed, we know that all Dogs are Pets
Dog* d = new Pet;  // this is not allowed, because Pet doesn't support everything Dog does
0 голосов
/ 10 августа 2016

Исходя из некоторых предыдущих ответов, у меня появилось некоторое понимание, и я выражаю их в своих словах. Based on some of previous answers I developed some understanding and I am putting them in my words

0 голосов
/ 22 июня 2013

Фактическая причина в том, что производный класс содержит всю информацию о базовом классе, а также некоторую дополнительную информацию.Теперь указатель на производный класс потребует больше места, а этого недостаточно в базовом классе.Таким образом, указатель на производный класс не может указывать на него.Хотя, с другой стороны, верно обратное.

0 голосов
/ 18 марта 2012

Все ответы выше хороши. Я просто хочу добавить еще одну вещь. Я думаю, что ваша диаграмма собака / домашнее животное вводит в заблуждение.

Я понимаю, почему вы нарисовали DOG-диаграмму как надмножество PET: вы, вероятно, подумали, что, поскольку у Dog больше атрибутов, чем у питомца, его необходимо представить большим набором.

Однако всякий раз, когда вы рисуете диаграмму, где набор B является подмножеством набора A, вы говорите, что число объектов типа B наверняка НЕТ БОЛЬШЕ, чем объектов типа A. С другой стороны, поскольку объекты типа B имеют больше свойств, вам разрешено выполнять с ними больше операций, поскольку вы можете выполнять ВСЕ операции, разрешенные для объектов типа A, ПЛЮС еще.

Если вам что-то известно о функциональном анализе (это длинный план, но, может быть, вы видели это), то же самое существует между банаховыми пространствами и их двойственными элементами: чем меньше пространство, тем больше набор операций, которые вы могу сделать на них.

0 голосов
/ 17 марта 2012

Рассмотрим этот сценарий (извините за такой глупый пример):

Автомобиль может быть любым транспортным средством

class Vehicle
{
   int numberOfWheels;
   void printWheels();
};

Автомобиль - это транспортное средство

class Car: public Vehicle
{
    // the wheels are inherited
    int numberOfDoors;
    bool doorsOpen;
    bool isHatchBack;
};

Велосипед - это транспортное средство, но это не автомобиль, который тоже является автомобилем

class Bike: public Vehicle
{
    int numberOfWings; // sorry, don't know exact name
    // the wheels are inherited
};

Поэтому я надеюсь, что вы не только увидите разницу в реальной жизни, но и заметите, что расположение памяти в программе будетотличаться для Bike и Car объектов, даже если они оба Vehicles.Вот почему дочерний объект не может быть дочерним типом;это может быть только то, что было определено.

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