Фундаментальная проблема - это копирование объекта (что не является проблемой в языках, где классы являются «ссылочными типами», но в C ++ по умолчанию передаются вещи по значению, т. Е. Делается копия). «Нарезка» означает копирование значения большего объекта (типа B
, который происходит от A
) в меньший объект (типа A
). Поскольку A
меньше, создается только частичная копия.
Когда вы создаете контейнер, его элементы являются полными собственными объектами. Например:
std::vector<int> v(3); // define a vector of 3 integers
int i = 42;
v[0] = i; // copy 42 into v[0]
v[0]
- это переменная int
, такая же как i
.
То же самое происходит с классами:
class Base { ... };
std::vector<Base> v(3); // instantiates 3 Base objects
Base x(42);
v[0] = x;
Последняя строка копирует содержимое объекта x
в объект v[0]
.
Если мы изменим тип x
следующим образом:
class Derived : public Base { ... };
std::vector<Base> v(3);
Derived x(42, "hello");
v[0] = x;
... затем v[0] = x
пытается скопировать содержимое объекта Derived
в объект Base
. В этом случае происходит то, что все члены, объявленные в Derived
, игнорируются. Копируются только члены данных, объявленные в базовом классе Base
, потому что это все, что v[0]
имеет место для.
Указатель дает вам возможность избежать копирования. Когда вы делаете
T x;
T *ptr = &x;
, ptr
не является копией x
, он просто указывает на x
.
Точно так же вы можете сделать
Derived obj;
Base *ptr = &obj;
&obj
и ptr
имеют разные типы (Derived *
и Base *
соответственно), но C ++ в любом случае разрешает этот код. Поскольку Derived
объекты содержат все элементы Base
, можно разрешить указателю Base
указывать на экземпляр Derived
.
По сути, это сокращенный интерфейс до obj
. При доступе через ptr
он имеет только методы, объявленные в Base
. Но поскольку копирование не было выполнено, все данные (включая Derived
определенные части) все еще там и могут использоваться для внутреннего использования.
Что касается virtual
: Обычно, когда вы вызываете метод foo
через объект типа Base
, он вызывает ровно Base::foo
(то есть метод, определенный в Base
). Это происходит, даже если вызов выполняется через указатель, который фактически указывает на производный объект (как описано выше) с другой реализацией метода:
class Base {
public:
void foo() const { std::cout << "hello from Base::foo\n"; }
};
class Derived : public Base {
public:
void foo() const { std::cout << "hello from Derived::foo\n"; }
};
Derived obj;
Base *ptr = &obj;
obj.foo(); // calls Derived::foo
ptr->foo(); // calls Base::foo, even though ptr actually points to a Derived object
Помечая foo
как virtual
, мы заставляем вызов метода использовать фактический тип объекта вместо объявленного типа указателя, через который осуществляется вызов:
class Base {
public:
virtual void foo() const { std::cout << "hello from Base::foo\n"; }
};
class Derived : public Base {
public:
void foo() const { std::cout << "hello from Derived::foo\n"; }
};
Derived obj;
Base *ptr = &obj;
obj.foo(); // calls Derived::foo
ptr->foo(); // also calls Derived::foo
virtual
не влияет на обычные объекты, потому что там объявленный тип и фактический тип всегда одинаковы. Он влияет только на вызовы методов, сделанные через указатели (и ссылки) на объекты, поскольку они могут ссылаться на другие объекты (потенциально разных типов).
И это еще одна причина для хранения коллекции указателей: если у вас есть несколько различных подклассов GameObject
, каждый из которых реализует свой собственный draw
метод, вы хотите, чтобы код обращал внимание на фактические типы объекты, поэтому правильный метод вызывается в каждом случае. Если бы draw
не было виртуальным, ваш код попытался бы вызвать GameObject::draw
, которого не существует. В зависимости от того, как именно вы его кодируете, он либо не будет компилироваться в первую очередь, либо прерываться во время выполнения.