It's perfectly fine to change what a pointer points to. A Base*
is not an instance of Base
, it is a pointer that points to an instance of a Base
(or something derived from it -- in this case B
or C
).
Thus in your code, base = new B()
sets it to point to a new instance of a B
, and then base = new C()
sets it to point to a new instance of a C
.
Derived classes are different from each other, so what happens in memory when the polymorphic pointer object changes?
Because Base*
points to an instance of a Base
, all this is doing is changing which instance (or derived instance) Base*
points to. In effect, it just changes the memory address of the pointer.
From that Base*
pointer, you still have access to anything defined in that Base
class -- which still allows polymorphic calls to functions satisfied by derived types if the function is defined as virtual
.
The exact mechanism for how this is dispatched to derived types is technically an implementation-detail of the language, but generally this is done through a process called double-dispatch, which uses a "V-Table". This is additional type-information stored alongside any classes that contain virtual
functions (it's conceptually just a struct
of function pointers, where the function pointers are satisfied by the concrete types).
See: Why do we need a virtual table? for more information on vtables.
What is problematic, however, is the use of new
here. new
allocates memory that must be cleaned up with delete
to avoid a memory leak. By doing the following:
Base *base = new B();
base->addTest();
base = new C(); // overwriting base without deleting the old instance
base->addTest();
The B
object's destructor is never run, no resources are cleaned up, and the memory for B
itself is never reclaimed. This should be:
Base *base = new B();
base->addTest();
delete base;
base = new C(); // overwriting base without deleting the old instance
base->addTest();
delete base;
Or, better yet, this should be using smart-pointers like std::unique_ptr
to do this for you. In which case you don't use new
and delete
explicitly, you use std::make_unique
for allocation, and the destructor automagically does this for you:
auto base = std::make_unique<B>();
base->addTest();
base = std::make_unique<C>(); // destroy's the old instance before reassigning
base->addTest();
This is the recommended/modern way to write dynamic allocations