2009-08-26 2 views
4

вы можете объяснить мне, почему:Насколько это виртуально?

int main (int argc, char * const argv[]) { 
    Parent* p = new Child(); 
    p->Method(); 
    return 0; 
} 

печатает "ребенка :: Метод()", а это:

int main (int argc, char * const argv[]) { 
    Parent p = *(new Child()); 
    p.Method(); 
    return 0; 
} 

печатает "Родитель :: Method()"?

Классы:

class Parent { 
public: 
    void virtual Method() { 
     std::cout << "Parent::Method()"; 
    } 
}; 

class Child : public Parent { 
public: 
    void Method() { 
     std::cout << "Child::Method()"; 
    } 
}; 

спасибо, Etam.

ответ

11

Ваш второй код экземпляры a Child объект в переменной Parent. По способу slicing он теряет всю информацию, относящуюся к Child (т. Е. Все частные поля, частичные к Child) и, как следствие, связанную с ним информацию о виртуальном методе.

Кроме того, оба ваших кода утечки памяти (но я думаю, вы знаете это).

Вы можете использовать ссылки. Например:

Child c; 
Parent& p = c; 
p.Method(); // Prints "Child::Method" 
+0

Я уверен, что нарезка не является причиной симптомов в этом случае. – quamrana

+1

Любой ответ на этот вопрос должен содержать концептуальное объяснение того, как полиморфизм применяется только к указателям и ссылкам, а во втором случае «p» не является ни тем, ни другим. Говоря о разрезе, вы не предоставляете «настоящий» ответ. Это побочный эффект того, что происходит, но не отвечает на исходный вопрос. – psychotik

+1

Я с @konrad. Настоящая причина заключается не в том, что у вас нет указателя или ссылки. Но настоящая причина заключается в том, что объект не относится к типу 'Child'. И это вызвано нарезкой. –

9

В первом случае вы называете реальный объект имеет класс детей:

Parent* p = new Child(); // you new'ed Child class 
p->Method(); // and a method of a Child class object is getting called 

, поэтому для детей :: Метод() вызывается. Во втором случае вы копируете объект класса Child на объект класса Родитель:

Parent p = *(new Child()); // you new'ed Child, then allocated a separate Parent object on stack and copied onto it 
p.Method(); // now you have a Parent object and its method is called 

и Parent :: Метод() вызывается.

+0

Вы прокомментировали, что происходит, что означает OP. Можете ли вы объяснить, почему это происходит? – quamrana

+0

Это потому, что виртуальный метод вызывается в зависимости от типа объекта, а не от типа указателя. В первом случае объект имеет тип класса Child, во втором - тип класса Parent. – sharptooth

+0

@sharptooth: Да, в первом случае есть вызов виртуального метода через указатель, но во втором случае нет вызова виртуального метода, только прямой вызов 'Parent :: Method()' – quamrana

2

Во втором примере происходит срез: экземпляр Child-instance преобразуется в родительский экземпляр, который не имеет записи vtable для Child-метода. Детский метод «разрезан».

+1

Первоначально я думал, что это нарезка, но я не думаю, что это механизм. Это просто не полиморфный вызов. – quamrana

+0

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

+0

@quamrana: Это не полиморфный вызов, потому что Parent не является производным типом. Ребенок создается через новое. Родитель создан в стеке, а затем устанавливается равным Ребенку (т. Е. Ребенок нарезается до родителя). Затем в родителе вызывается метод. О, и память от новички Ребенка просочилась. –

1

В первом случае у вас есть указатель «Родитель» на объекте «Ребенок», который является каким-то общим, поскольку «Ребенок» наследуется от «Родитель». Следовательно, вызывается перегруженный метод «Child».

Во втором случае вы создаете неявный приведение экземпляра «Child» к типу «Родитель», поэтому вы вызываете метод на объекте «Родитель».

Надеюсь, что это поможет.

0

В первом примере вы вызываете метод через указатель:

p->Method(); 

В этом случае «р» является указателем на Родитель, и C++ знает, что Родитель имеет V-таблицу , поэтому он использует это, чтобы найти фактический метод для вызова, который в этом случае, как вы заявляете, является Child::Method(), потому что, когда C++ находит v-таблицу, он находит v-таблицу «нового» экземпляра Child.

Во втором примере вы вызываете метод на примере:

p.Method(); 

В этом случае «р» является экземпляром Родитель и C++ предполагает, что он знает, что точный тип действительно «Родитель» и так он звонит Parent::Method() без прохождения каких-либо v-таблиц.

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

Parent& q=p; 
q.Method(); 

Потом я вижу: Parent :: Метод() печатаемого. q.Method() должен быть виртуальным вызовом, но он может найти только v-таблицу для родителя.

3

Виртуальное поведение доступно, только если виртуальная функция - , вызываемая указателем или ссылкой. Когда вы говорите:

Parent p = *(new Child()); 
p.Method(); 

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

+0

@Neil: виртуальные методы будут называться просто отлично без указателя или ссылки. Объявите ребенка в стеке, и его виртуальные методы будут называться просто spiffy. –

+0

Виртуальный метод будет вызываться, но вы не получите виртуального поведения. – 2009-08-26 09:36:02

+1

@ Майкл: Да, будут вызваны виртуальные методы, но, как сказал Нил (я просто перефразирую), вы не получите полиморфного поведения, потому что в этом случае не будет поискать vtable. – Aamir

0

В первом случае тип объекта, на который указывает указатель p, имеет тип «Child». Поскольку дочерний элемент переопределяет виртуальный метод, определенный в базовом классе, вызывается метод child. Во втором случае у вас есть , который копирует дочерний объект в родительский объект. Здесь происходит разбиение объектов, и тип результирующего объекта имеет тип «Родитель». Следовательно, когда вы вызываете метод, вызывается метод parent.

1

Полиморфизм в C++ возможен только с указателями. Различие между статическим и динамическим типом (в вашем первом примере p имеет статический тип Parent *, но динамического типа Child *, и это позволяет C++ вызвать метод производного класса) не вступает в игру с не указателями ,

+1

«Полиморфизм в C++ возможен только с указателями». ... и ссылки. Это * не * указатели. –

+0

Да, ты прав. Скажем, «с эталонной семантикой». – Thomas

3

Что произойдет, если вы сделаете это?

int main (int argc, char * const argv[]) { 
    Parent &p = *(new Child()); 
    p.Method(); 
    return 0; 
} 

То есть один и тот же «синтаксический» эффект (p не требует разыменований, чтобы использовать его), но семантический эффект не является в настоящее время полностью отличается, потому что вы больше не копировать часть Child в Parent.

+0

Как и указатель от OP, ваш Parent & действительно является только указателем, поэтому никакие биты Ребенка не копируются, а только его адрес. Теперь вызов p.Method() является полиморфным. – quamrana

+0

Это действительно плохой код, так как он пропускает память, которая никогда не может быть восстановлена ​​надежным способом (конечно, вы * можете * вызывать 'delete & p', но это нестандартный код). –

+0

@ Konrad: Можем ли мы оставить утечки памяти из этого на данный момент? – quamrana

2

Ваш первый случай прост. Экземпляр Ребенка создается и присваивается p. Поэтому вызов p-> Method() вызывает Child :: Method().

Во втором случае, четыре вещи:

  1. Экземпляр класса детей, которые были определены временной переменной компилятором присваивается, создается.
  2. Создан экземпляр класса Parent, идентифицированный переменной p.
  3. Создатель копирования Parent :: Parent (родительский &) вызывается, когда p создается для копирования родительского «среза» состояния экземпляра Child на p. Обратите внимание, что если вы не определяете этот конструктор копирования, тогда компилятор создает его для вас.
  4. Вы вызываете метод() на p, который является экземпляром Родитель.

Попробуйте явно определить конструктор копирования, и вы увидите, что он вызывается.

Ваше возможное замешательство, вероятно, связано с тем, что задания (=) в двух примерах делают разные вещи. В первом примере он просто устанавливает один указатель, равный другому, и есть только один объект. Во втором нет указателей, поэтому вы назначаете (нарезку) значение. Это вызывает конструктор копирования, и вы получаете два объекта (ребенок и родитель). Тот факт, что компилятор создает невидимую временную переменную, не помогает понять, что происходит. Вы можете проверить статью this о конструкторах копий.

Это легкая ошибка, если вы привыкли к языкам, таким как Java или C#, где в основном все является ссылкой (также называемой указателем).

Как говорили другие, полиморфизм работает только с указателями и ссылками, и они не используются в вашем втором примере.