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

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

предположим, что мы имеем следующую структуру классов

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

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

a (Dog) is a (Pet)

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

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 - Pet на диаграмме выше).

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

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

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

11 ответов


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

понимание is a в C++ (полиморфизм, в общем) неверно.

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

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


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

Pet* p = new Dog;

указатель p определен пункт любой Пэт-совместимый объект, который в 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 не должен быть разрешен без предупреждений потому что нет никакого способа, чтобы указатель мог указывать на объект типа своего superset (объект собаки superset любимчика) просто потому что питомец ничего не знает о новых членах этой собаки мог бы объявить (подмножество Dog-Pet на диаграмме выше).

указатель p ожидает найти [pet data] в месте, на которое он указывает. Поскольку правая сторона-это Dog, и все и 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 включен.


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

дополнительная информация


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

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

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


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


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

A Dog is-a Pet

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


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

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

С другой стороны, если у меня есть Pet*, Я понятия не имею, на что это на самом деле указывает. Это может указывать на Dog, но это может также указать Cat, a Fish, или что-то совсем другое. Таким образом, язык не позволяет мне позвонить Pet->bark() потому что не все Pets can bark() (Cats 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

на английском, высказывания вы пытаетесь отработать могут быть аспекты собака, которая вынуждает его быть домашнее животное, являются подмножеством всех аспектов собака " и " совокупность всех лиц, которые являются собаки являются подмножеством набора сущностей, которые домашних.

D, P такое, что D(x) => x Dog Dog, P (x) => x

D(x) => P (x)

(D(x) истинно, если x имеет все аспекты собаки, поэтому это говорит о том, что аспекты вещи, которая является собакой, являются супер множество аспектов вещей, которые являются домашними животными-p (x) истинно, если d (x) истинно, но не обязательно наоборот)

Собака Pet Домашнее Животное =>

Dog x x Dog Dog => X Pets домашние животные (каждая собака домашнее животное)

но если d (x) Dog X Dog Dog, то это одно и то же утверждение.

сказав 'аспектов собаки, которые делают его питомца являются подмножеством собаки в целом-это равносильно тому 'набор собак является подмножеством множества животных


рассмотрим такой сценарий (извините за такой дрянной пример):

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

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. Вот почему дочерний объект не может быть любым типом ребенка; он может быть только тем, что было определено.


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

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

однако всякий раз, когда вы рисуете диаграмму, где множество B является подмножеством множества A, вы говорите, что количество объектов типа B наверняка не больше, чем объекты типа А. С другой стороны, поскольку объекты типа B имеют больше свойств, вам разрешено выполнять больше операций над ними, поскольку вы можете выполнять все операции, разрешенные для объектов типа A плюс еще несколько.

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


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


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