So I am refreshing on C++, and honestly it's been awhile. I made a console pong game as a sort of refresher task and got some input on using polymorphism for my classes to derive from a base "GameObject" (that has some base methods for drawing objects to the screen).
One of the pieces of input was (and I had subsequently asked about) was how memory worked when deriving from base classes. Since I hadn't really done much advanced C++.
For instance lets say we have a base class, for now it just has a "draw" method (Btw why do we need to say virtual
for it?), since all other derived objects really only share one common method, and that's being drawn:
class GameObject
{
public:
virtual void Draw( ) = 0;
};
we also have a ball class for instance:
class Ball : public GameObject
The input I received is that in a proper game these would probably be kept in some sort of vector of GameObject pointers. Something like this: std::vector<GameObject*> _gameObjects;
(So a vector of pointers to GameObjects) (BTW Why would we use pointers here? why not just pure GameObjects?). We would instantiate one of these gameObjects with something like:
_gameObjects.push_back( new Ball( -1, 1, boardWidth / 2, boardHeight / 2 ); );
(new
returns a pointer to the object correct? IIRC). From my understanding if I tried to do something like:
Ball b;
GameObject g = b;
That things would get messed up (as seen here: What is object slicing?)
However...am I not simply creating Derived objects on their own when I do the new Ball( -1, 1, boardWidth / 2, boardHeight / 2 );
or is that automatically assigning it as a GameObject too? I can't really figure out why one works and one doesn't. Does it have to do with creating an object via new
vs just Ball ball
for example?
Sorry if the question makes no sense, im just trying to understand how this object slicing would happen.
The fundamental issue is copying an object (which is not an issue in languages where classes are "reference types", but in C++ the default is to pass things by value, i.e. making a copy). "Slicing" means copying the value of a bigger object (of type B
, which derives from A
) into a smaller object (of type A
). Because A
is smaller, only a partial copy is made.
When you create a container, its elements are full objects of their own. For example:
std::vector<int> v(3); // define a vector of 3 integers
int i = 42;
v[0] = i; // copy 42 into v[0]
v[0]
is an int
variable, just like i
.
The same thing happens with classes:
class Base { ... };
std::vector<Base> v(3); // instantiates 3 Base objects
Base x(42);
v[0] = x;
The last line copies the contents of the x
object into the v[0]
object.
If we change the type of x
like this:
class Derived : public Base { ... };
std::vector<Base> v(3);
Derived x(42, "hello");
v[0] = x;
... then v[0] = x
tries to copy the contents of a Derived
object into a Base
object. What happens in this case is that all members declared in Derived
are ignored. Only the data members declared in the base class Base
are copied, because that's all v[0]
has room for.
What a pointer gives you is the ability to avoid copying. When you do
T x;
T *ptr = &x;
, ptr
is not a copy of x
, it just points to x
.
Similarly, you can do
Derived obj;
Base *ptr = &obj;
&obj
and ptr
have different types (Derived *
and Base *
, respectively), but C++ allows this code anyway. Because Derived
objects contain all members of Base
, it's OK to let a Base
pointer point at a Derived
instance.
What this gives you is essentially a reduced interface to obj
. When accessed through ptr
, it only has the methods declared in Base
. But because no copying was done, all data (including the Derived
specific parts) are still there and can be used internally.
As for virtual
: Normally, when you call a method foo
through an object of type Base
, it will invoke exactly Base::foo
(i.e. the method defined in Base
). This happens even if the call is made through a pointer that actually points at a derived object (as described above) with a different implementation of the method:
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
By marking foo
as virtual
, we force the method call to use the actual type of the object, instead of the declared type of the pointer the call is made through:
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
has no effect on normal objects because there the declared type and the actual type are always the same. It only affects method calls made through pointers (and references) to objects, because those have the ability to refer to other objects (of potentially different types).
And that is another reason to store a collection of pointers: When you have several different subclasses of GameObject
, all of which implement their own custom draw
method, you want the code to pay attention to the actual types of the objects, so the right method gets called in each case. If draw
weren't virtual, your code would attempt to invoke GameObject::draw
, which doesn't exist. Depending on how exactly you code it, this either wouldn't compile in the first place or abort at runtime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With