Полиморфизм в С++: вызов переопределенного метода

Во-первых, я Java-кодер и хочу понять полиморфизм в С++. Я написал пример для учебных целей:

#include<iostream>

using namespace std;

class A
{
public:
    virtual void foo(){ std::cout << "foo" << std::endl; }
};

class B : public A
{
public:
    void foo(){ std::cout << "overriden foo" << std::endl; }
};

A c = B(); 

int main(){ c.foo(); } //prints foo, not overriden foo

Я ожидал, что overriden foo будет напечатан, но это не так. Зачем? Мы переопределили метод foo в class B, и я подумал, что решение, которое следует вызывать, производится из типа среды выполнения, который в моем случае равен B, но не статический тип ( в моем случае).

Живой пример там

Ответы

Ответ 1

Когда вы это сделаете:

A c = B(); 

Вы конвертируете значение B в A. Вы этого не хотите.

Вы должны сделать объект B и получить доступ к нему с помощью указателя A или ссылки, чтобы получить полиморфное поведение:

B b;
A& c = b;

Ответ 2

В java у вас есть семантика значений с типами int и float, и у вас есть ссылочная семантика со всем остальным.

Это не так в C++: система типов унифицирована, и вы получаете какую-либо ценность или ссылочную семантику, о которой вы просите.

С кодом, который вы написали

A c = B()

вы сказали компилятору создать новое значение типа B, а затем преобразовать его в значение типа A, сохранив значение в c. Преобразование в этом случае означает получение данных A из нового экземпляра B, который вы создали, и копирование его в новый экземпляр A, хранящийся в c.

Вместо этого вы можете сделать это:

B b;
A &c = b;

Это все еще создает значение B, но теперь c является ссылкой на A, что означает, что c теперь будет ссылаться на созданный вами экземпляр B, а не на копию его A часть.

Теперь это все равно создает B как локальную переменную, а объект, хранящийся в B, уничтожается, как только B выходит за пределы области видимости. Если вы хотите что-то более настойчивое, вам нужно будет использовать указатели; например что-то вроде

shared_ptr<A> c = make_shared<B>();
c->foo();

Вы можете сделать что-то более "сырое", например

A *b = new B();

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

Ответ 3

Ваше замешательство проистекает из решающего различия между Java и С++.

В Java, если вы пишете

MyClass var = whatever;

Ваша переменная var является ссылкой на объект, возвращаемый whatever. Однако в С++ этот синтаксис означает "создать новый объект типа MyClass, передав результат выражения whatever соответствующему конструктору и скопировав полученный объект в переменную var.

В частности, ваш код создает новый объект типа A, называемый c, и передает его созданный по умолчанию объект типа B в его конструктор копирования (потому что это единственный конструктор, который подходит). Поскольку вновь созданный объект имеет тип A, не вызывается тип B, очевидно A метод foo.

Если вы хотите иметь ссылку на объект, вы должны явно запросить его в С++, добавив & к типу. Однако ссылка на непостоянные объекты не может быть привязана к временным. поэтому вам нужно явно объявить объект, к которому вы привязываетесь (или, альтернативно, использовать ссылку на объект const и исправить свои функции-члены foo const, так как они все равно не изменяют объект). Поэтому простейшая версия вашего кода, выполняющая то, что вы хотите, будет читать:

// your original definitions of A and B assumed here

B b; // The object of type B
A& c = b; // c is now a *reference* to b
int main() { c.foo(); } // calls B::foo() thanks to polymorphism

Однако лучшая версия была бы const-correct, а затем могла бы использоваться ваша оригинальная конструкция:

#include <iostream>

class A
{
public:
    virtual void foo() const  // note the additional const here!
    { std::cout << "foo" << std::endl; }
};

class B : public A
{
public:
    void foo() const // and also const here
    { std::cout << "overridden foo" << std::endl; }
};

A const& c = B(); // Since we bind to a const reference,
                  // the lifetime of the temporary is extended to the
                  // lifetime of the reference

int main() { c.foo(); } //prints overridden foo

(обратите внимание, что я удалил using namespace std;, потому что это плохо (и ваш код использовал явный std:: в любом случае, поэтому он просто лишний).

Обратите внимание, что ссылки на С++ по-прежнему отличаются от ссылок на Java тем, что они не могут быть переназначены; вместо этого любое присваивание переходит к базовому объекту. Например:

#include <iostream>

class A { public: virtual void foo() const { std::cout << "I'm an A\n"; } };
class B: public A { public: void foo() const { std::cout << "I'm a B\n"; } };
class C: public A { public: void foo() const { std::cout << "I'm a C\n"; } };

B b;
C c;

int main()
{
   A& ref = b; // bind reference ref to object b
   ref.foo(); // outputs "I'm a B"
   ref = c; // does *not* re-bind the reference to c, but calls A::operator= (which in this case is a no-op)
   ref.foo(); // again outputs "I'm a B"
}

Если вы хотите изменить объект, на который вы ссылаетесь, вам придется использовать указатели:

// definitions of A, B and C as above
int main()
{
  A* prt = &b; // pointer ptr points to b
  prt->foo();  // outputs "I'm a B"
  prt = &c;    // reassign ptr to point to c
  prt->foo();  // outputs "I'm a C"
}

Ответ 4

Интересующая строка - это (вместо этого используется синтаксис с синтаксисом):

A c = B{};

Важно отметить, что при объявлении этого способа c ведет себя как значение.

Ваш код строит локальный A с именем c из экземпляра B. Это называется slicing: любая часть этого B, которая также не является частью A, была "нарезана", оставив только A.

Предоставление семантики ссылок на символы в С++ (называемое косвенным) требует различной нотации.

Например:

A &c = B{};
A d = B{}; // Casts the intermediate B instance to const A &,
           // then copy-constructs an A

c.foo(); // calls B::foo because c points to a B through an A interface

d.foo(); // calls A::foo because d is only an instance of an A

Обратите внимание, что время жизни промежуточного B, к которому точки c автоматически расширяется до области c. С другой стороны, второй промежуток B разрушается после завершения построения d.

В С++ ссылки являются неизменяемыми (они не могут быть изменены после инициализации). При использовании в выражении, как будто объект (значение), к которому они указывают, был использован вместо:

A &c = B{};

c = A{}; // Calls the compiler-provided A::operator = (const A &)
         // (a virtual assignment operator with this signature
         // was not defined).
         // This DOES NOT change where c points.

Указатели, с другой стороны, могут быть изменены:

A a{};
B b{};

A *cptr = &b;

cptr->foo(); // calls B::foo

cptr = &a;

cptr->foo(); // calls A::foo

Ответ 5

точно, что сказал orlp. вы также должны научиться использовать указатели (они забавны)

A* c = new B();
c->foo(); // overriden foo
delete c;