Предположим, что у меня есть класс Dog
это наследовалось классу Animal
. Каково различие между этими двумя строками кода?
Animal *a = new Dog();
Dog *d = new Dog();
В одном указатель для базового класса, и в другом, указатель для производного класса. Но когда это различие стало бы важным? Для полиморфизма любой работал бы точно то же, правильно?
Для всех целей проверки типов компилятор обрабатывает a
так, как если бы он мог указывать на любое животное, даже если вы знаете, что он указывает на собаку:
a
функции, ожидающей Dog *
. a-> fetchStick ()
, где fetchStick
является функцией-членом Dog
, но не Animal
. . Dog * d2 = dynamic_cast (d)
, вероятно, просто копия указателя на вашем компиляторе. Dog * d3 = dynamic_cast (a)
, вероятно, нет (я размышляю здесь, я не собираюсь проверять какой-либо компилятор. Дело в том, что компилятор, вероятно, делает другое предположения о a
и d
при преобразовании кода). Вы можете вызывать виртуальные функции (то есть определенный полиморфный интерфейс) Animal одинаково через любую из них с одинаковым эффектом. Предположим, что Dog
все равно их не спрятал (хороший аргумент, JaredPar).
Для невиртуальных функций, которые определены в Animal, а также определены (перегружены) в Dog, вызов этой функции через a
отличается от вызова через d
.
Ответ на этот вопрос гигантский: это зависит
Существует множество способов, при которых тип указателя может стать важным. C ++ - очень сложный язык, и один из способов его проявления - наследование.
Давайте возьмем небольшой пример, чтобы продемонстрировать один из многих способов, которыми это может иметь значение.
class Animal {
public:
virtual void MakeSound(const char* pNoise) { ... }
virtual void MakeSound() { ... }
};
class Dog : public Animal {
public:
virtual void MakeSound() {... }
};
int main() {
Animal* a = new Dog();
Dog* d = new Dog();
a->MakeSound("bark");
d->MakeSound("bark"); // Does not compile
return 0;
}
Причина в том, что C ++ выполняет поиск имен из-за своей причуды. Вкратце: при поиске метода для вызова C ++ будет проходить по иерархии типов в поисках первого типа, у которого есть метод с совпадающим именем. Затем он будет искать правильную перегрузку из методов с таким именем, объявленных для этого типа. Поскольку Dog
объявляет только метод MakeSound
без параметров, перегрузка не совпадает, и он не может быть скомпилирован.
Нет, это не одно и то же.
Указатель Dog не такой полиморфный, как указатель Animal. Все, на что он может указывать во время выполнения, - это Dog или подкласс Dog. Если подклассов Dog нет, то тип среды выполнения Dog и типы времени компиляции совпадают.
Указатель Animal может относиться к любому подклассу Animal: Dog, Cat, Wildebeast и т. Д.
Это не имеет значения во время выполнения, поскольку два экземпляра одинаковы. Единственная разница во время компиляции, когда вы можете вызвать, например, d-> bark (), но не a-> bark (), даже если a действительно содержит собаку. Компилятор считает переменную животным и только этим.
Вы всегда должны помнить, что в каждом классе есть 2 части: данные и интерфейс.
Ваш код действительно создал 2 объекта Dog в куче. Это означает, что данные принадлежат собаке. Этот объект имеет размер, равный сумме всех членов данных Dog + Animal + указатель vtable.
Понтеры a и d (значения l) различаются с точки зрения интерфейса. Это определяет, как вы можете относиться к ним с точки зрения кода. Таким образом, даже если Animal * a на самом деле Dog, вы не могли получить доступ к a-> Bark (), даже если Dog :: Bark () существовал. d-> Bark () работал бы нормально.
Добавление vtable обратно в картинку, предполагая, что интерфейс Animal имеет Animal :: Move универсальный Move () и что Dog действительно перезаписан Dog :: Move () {как собака}.
Даже если бы у вас было Animal a * и вы выполнили a-> Move () благодаря vtable, вы бы фактически использовали Move () {как собака}. Это происходит потому, что Animal :: Move () был (виртуальным) указателем на функцию, перенаправленным на Dog's :: Move () при построении Dog ().
Первая строка позволяет вам вызывать только члены класса Animal в a:
Animal *a = new Dog();
a->eat(); // assuming all Animal can eat(), here we will call Dog::eat() implementation.
a->bark(); // COMPILATION ERROR : bark() is not a member of Animal! Even if it's available in Dog, here we manipulate an Animal.
Хотя (как указано другими), в этом случае как a
по-прежнему является Animal, вы не можете предоставить a
в качестве параметра функции, запрашивающей более конкретный дочерний класс, которым является Dog:
void toy( Dog* dog );
toy( a ); // COMPILATION ERROR : we want a Dog!
Вторая строка позволяет вам использовать конкретные функции дочернего класса:
Dog *a = new Dog();
a->bark(); // works, but only because we're manipulating a Dog
Так что используйте базовый класс как «общий» интерфейс иерархии классов (позволяющий заставить всех ваших Animals to eat (), не беспокоясь о том, как).
Это различие важно, когда вы вызываете виртуальную функцию с помощью указателя. Допустим, у Animal и Dog есть функции do_stuff ().
Если Animal :: do_stuff () объявлен виртуальным, вызов do_stuff () для указателя Animal вызовет Dog :: do_stuff ().
Если Animal :: do_stuff () не объявлен виртуальным, вызов do_stuff () для указателя Animal вызовет Animal :: do_stuff ().
Вот полная рабочая программа для демонстрации:
#include <iostream>
class Animal {
public:
void do_stuff() { std::cout << "Animal::do_stuff\n"; }
virtual void virt_stuff() { std::cout << "Animal::virt_stuff\n"; }
};
class Dog : public Animal {
public:
void do_stuff() { std::cout << "Dog::do_stuff\n"; }
void virt_stuff() { std::cout << "Dog::virt_stuff\n"; }
};
int main(int argc, char *argv[])
{
Animal *a = new Dog();
Dog *b = new Dog();
a->do_stuff();
b->do_stuff();
a->virt_stuff();
b->virt_stuff();
}
Вывод:
Animal::do_stuff
Dog::do_stuff
Dog::virt_stuff
Dog::virt_stuff
Это всего лишь один пример. В других ответах перечислены другие важные отличия.
Разница важна, когда вы пытаетесь вызвать методы Dog, которые не являются методом Animal. В первом случае (указатель на Animal) вы должны сначала указать указатель на Dog. Другое отличие заключается в том, что вы случайно перегружаете невиртуальный метод. Затем будет вызван либо Animal :: non_virtual_method () (указатель на Animal), либо Dog :: non_virtual_method (указатель на Dog).