inheritance and pointers

In C++, inheritance enables derived classes to extend or override behavior from a base class. But to use inheritance correctly — especially when dealing with polymorphism — you must use pointers or references, not values. This article explains why object slicing occurs, why destructors should be virtual, and how smart pointers help manage lifetimes.

Object Slicing: What It Really Means

Consider the following classes:

class Base {
public:
    virtual std::string type() const { return "Base"; }
};

class Derived : public Base {
public:
    std::string type() const override { return "Derived"; }
    std::string derived_info = "extra";
};

Now do this:

Derived d;
Base b = d;
std::cout << b.type(); // prints "Base"

Although d is a Derived object, the assignment Base b = d performs a copy into a Base object. This invokes the Base copy constructor, which only copies the Base portion of d. Everything defined in Derived — data and overrides — is sliced off.

This is called object slicing. After slicing, the object is just a pure Base, and virtual dispatch no longer works as expected.

How to Avoid Slicing

Use a pointer or reference to access the object:

Derived d;
Base* ptr = &d;
std::cout << ptr->type(); // prints "Derived"

Base& ref = d;
std::cout << ref.type(); // also prints "Derived"

These avoid copying entirely. Instead, they access the original Derived object polymorphically. Thanks to the virtual table (vtable), method calls like type() are dispatched at runtime to the appropriate override.

Why Use std::shared_ptr or std::unique_ptr?

Raw pointers work, but they come with manual lifetime management. Smart pointers solve that:

std::shared_ptr<Base> b = std::make_shared<Derived>();
std::cout << b->type(); // prints "Derived"

shared_ptr ensures the object is kept alive as long as needed, preventing memory leaks and dangling pointers. Similarly, unique_ptr provides exclusive ownership with automatic destruction.

Why the Base Class Needs a Virtual Destructor

Consider this:

class Base {
public:
    ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived destroyed\n"; }
};

Base* b = new Derived();
delete b; // Only Base::~Base() runs!

This code has a bug. Since Base's destructor is not virtual, deleting via a Base* only calls Base's destructor. Derived's destructor is skipped entirely.

This is dangerous: Derived may hold resources like heap allocations or file handles that need to be cleaned up. Skipping its destructor can cause leaks or undefined behavior.

Correct Version:

class Base {
public:
    virtual ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived destroyed\n"; }
};

Base* b = new Derived();
delete b; // Calls both Derived and Base destructors

By making the destructor virtual, the call to delete b; uses the vtable to determine the correct destructor sequence, ensuring both destructors run in the correct order.

Why This Works: Static vs Dynamic Dispatch

Summary

These practices ensure inheritance works as intended and avoids subtle, dangerous bugs.


edit this page