← Back to all posts

15 C++ Notes

Polymorphism

C++ Notes


What Is Polymorphism

C++
class Base { virtual void method(); };
class Derived : public Base { void method() override; };

Base* obj = new Derived();
obj->method(); // calls Derived::method() at runtime

Static Binding vs Dynamic Binding

Static Binding (compile time)Dynamic Binding (runtime)
Method call resolved at compile time. The compiler uses the declared pointer or reference type to decide which method runs, it doesn't care about the actual object at runtime. This is the default behavior in C++ for all non-virtual functions, and it's faster since there's no runtime overhead. Method call resolved at runtime. The compiler uses the actual object type to decide which method runs. Requires the virtual keyword on the base method, and works through base pointers and references managing derived objects. Slightly slower due to the vtable lookup.
C++
Shape s;
Shape* ptr = &s;
Shape& ref = s;

ptr->display(); // virtual: resolved at runtime based on actual object
ptr->info();    // non-virtual: resolved at compile time based on pointer type

virtual and override Keywords

C++
class Shape {
public:
    virtual void display(); // virtual on base
};

class Circle : public Shape {
public:
    void display() override; // override on derived
};

Overloading, Overriding, and Hiding

OverloadingOverridingHiding
Multiple versions of the same function name in one class, with different parameter types or counts. Resolved at compile time. Same class only. A new implementation of a base virtual function in a derived class, with the same name and same signature. Resolved at runtime via the vtable. Requires virtual in the base. The derived class defines a function with the same name as the base, but the base function is not virtual. The base version is hidden, not overridden, no polymorphism happens, a base pointer still calls the base version.

Overriding one overload of a virtual function hides all other overloads from the base in the child class. If you need the other overloads too, you must explicitly override each one, or bring them back with using Base::functionName; inside the derived class. Overloads created only in the child class also don't participate in polymorphism at all, a base pointer can't call them even if the object is actually of the derived type.

Vtable and Size Overhead

How a virtual call works at runtime Shape* ptr = new Circle(); ptr->display(); Step 1: CPU reads vptr inside the Circle object Step 2: vptr points to Circle's vtable Step 3: vtable entry for display() points to Circle::display Step 4: Circle::display() runs Result: correct method called despite base pointer type

Object Slicing

C++
Circle circle(5);
Shape shape = circle; // slicing: Circle-specific data is lost
shape.display(); // calls Shape::display(), NOT Circle::display()

Always use pointers or references when working with polymorphism. Storing by value causes slicing and silently breaks dynamic dispatch. Arrays of base class objects cause slicing too, the moment you push derived objects into them.

Polymorphic Objects in Collections

C++
Circle c1(5), c2(10), c3(15);

// Wrong: slicing happens, derived part is lost
// Shape shapes[]{c1, c2, c3};

// Wrong: arrays cannot hold references
// const Shape& shapes[]{c1, c2, c3};

// Correct: array of base pointers
Shape* shapes[] = { &c1, &c2, &c3 };
for (int i = 0; i < 3; i++) {
    shapes[i]->display(); // calls Circle::display() for each
}

Static Members in Polymorphism

The final Keyword

final on a classfinal on a method
No other class can inherit from this class, it's the last in the inheritance chain. You can still override virtual functions inside the final class itself. Syntax: class Shape final { ... }; No further derived class can override this specific method, but the class itself is still inheritable. Useful when you want to lock down one specific behavior while allowing other extensions. Syntax: virtual void display() final;

Virtual Functions with Default Arguments

C++
class Shape {
public:
    virtual void display(int x = 10); // base default
};

class Circle : public Shape {
public:
    void display(int x = 20) override; // derived default (ignored via base pointer)
};

Shape* ptr = new Circle();
ptr->display(); // calls Circle::display BUT uses x=10 (base default!)

To avoid confusion, don't define different default arguments in base and derived virtual functions. Either keep them the same, or avoid default arguments on virtual functions altogether and use explicit calls instead.

Virtual Destructors

C++
class Shape {
public:
    virtual ~Shape() { /* cleanup */ } // virtual destructor
};

Shape* s = new Circle(7);
delete s;
// with virtual: Circle destructor runs first, then Shape destructor
// without virtual: ONLY Shape destructor runs (Circle resources leak)

Dynamic Cast (Downcasting)

C++
Shape* shape = new Circle(5);

Circle* circle = dynamic_cast<Circle*>(shape);

if (circle != nullptr) {
    circle->circleOnlyMethod(); // safe to use
} else {
    std::cout << "Cast failed: object is not a Circle\n";
}

Never Call Virtual Functions from Constructors or Destructors

This is a well-known C++ trap. The code compiles cleanly and looks correct, but the virtual call resolves statically to the base version. Never call virtual functions from constructors or destructors.

Pure Virtual Functions and Abstract Classes

C++
class Shape {
public:
    virtual double area() const = 0; // pure virtual: no body here
    virtual ~Shape() = default;
};

// Shape s;  ERROR: cannot instantiate abstract class

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
};

Shape* s = new Circle(5); // base pointer to derived: works fine

Abstract Classes as Interfaces

C++
class Drawable {
public:
    virtual void draw() = 0;    // pure virtual: must be implemented
    virtual ~Drawable() = default;
};

class Square : public Drawable {
public:
    void draw() override { std::cout << "Drawing Square\n"; }
};

Drawable* d = new Square();
d->draw(); // prints: Drawing Square

Code Example: main.cpp

C++
#include <iostream>

// ============================================================
// BASE CLASS: virtual display (dynamic), info (static)
// ============================================================
class Shape {
public:
    virtual void display() {   // virtual: dynamic binding
        std::cout << "Shape display\n";
    }

    void info() {               // non-virtual: static binding
        std::cout << "Shape info\n";
    }

    virtual ~Shape() {          // virtual destructor: REQUIRED for polymorphism
        std::cout << "Shape destroyed\n";
    }
};

// ============================================================
// DERIVED CLASS
// ============================================================
class Circle : public Shape {
private:
    int radius;
public:
    Circle(int r = 0) : radius(r) {}

    void display() override {  // overrides Shape::display
        std::cout << "Circle display, radius: " << radius << '\n';
    }

    void info() {               // hides Shape::info (NOT overriding)
        std::cout << "Circle info\n";
    }

    ~Circle() {
        std::cout << "Circle destroyed\n";
    }
};

// ============================================================
// STATIC MEMBERS
// ============================================================
class Counter {
public:
    static int count;
    Counter() { count++; }
    static void showCount() { std::cout << "Count: " << count << '\n'; }
};
int Counter::count = 0;

// ============================================================
// VIRTUAL WITH DEFAULT ARGUMENT (subtle gotcha)
// ============================================================
class Base {
public:
    virtual void greet(int x = 10) { // base default: 10
        std::cout << "Base greet: " << x << '\n';
    }
};

class Derived : public Base {
public:
    void greet(int x = 20) override { // derived default: 20 (ignored via base ptr)
        std::cout << "Derived greet: " << x << '\n';
    }
};

// ============================================================
// PURE VIRTUAL (abstract base) and INTERFACE
// ============================================================
class Drawable {      // pure interface: no data, no body
public:
    virtual void draw() = 0;
    virtual ~Drawable() = default;
};

class Square : public Drawable {
public:
    void draw() override { std::cout << "Drawing Square\n"; }
};

// ============================================================
// MAIN
// ============================================================
int main() {
    std::cout << "\n--- Static vs Dynamic Binding ---\n";
    Shape s;
    Shape* sp = &s;
    Shape& sr = s;
    s.display();    // dynamic: Shape::display
    sp->display(); // dynamic: Shape::display
    sr.display(); // dynamic: Shape::display
    s.info();      // static: Shape::info (pointer type decides)
    sp->info();    // static: Shape::info

    std::cout << "\n--- Polymorphic Collection ---\n";
    Circle c1(5), c2(10), c3(15);
    Shape* shapes[] = { &c1, &c2, &c3 }; // base pointers to derived objects
    for (int i = 0; i < 3; i++) {
        shapes[i]->display(); // calls Circle::display for each
    }

    std::cout << "\n--- Object Slicing ---\n";
    Shape sliced = c1;  // Circle-specific members stripped
    sliced.display();   // calls Shape::display (dynamic dispatch lost)

    std::cout << "\n--- Name Hiding (info is non-virtual) ---\n";
    c1.info();           // Circle::info (direct call on Circle object)
    shapes[0]->info(); // Shape::info (base pointer, non-virtual = static binding)

    std::cout << "\n--- Static Members ---\n";
    Counter a, b;
    Counter::showCount(); // prints: Count: 2

    std::cout << "\n--- Virtual Default Argument Gotcha ---\n";
    Base* bptr = new Derived();
    bptr->greet(); // calls Derived::greet but uses x=10 (base default!)
    delete bptr;

    std::cout << "\n--- Pure Virtual / Interface ---\n";
    Square sq;
    Drawable* dptr = &sq;
    dptr->draw(); // prints: Drawing Square

    std::cout << "\n--- Virtual Destructor ---\n";
    Shape* s2 = new Circle(7);
    delete s2;
    // virtual destructor: Circle destructor runs first, then Shape destructor
    // without virtual: only Shape destructor would run (Circle resources leak)

    std::cout << "\n--- Object Sizes (vtable overhead) ---\n";
    std::cout << "sizeof(Shape):  " << sizeof(Shape)  << "\n";
    std::cout << "sizeof(Circle): " << sizeof(Circle) << "\n";
    // Circle is larger: includes Shape data + int radius + vptr

    return 0;
}

Closing

That's the full C++ series, ground up from hello world to vtables and abstract interfaces. Polymorphism is really where everything from the earlier posts, pointers, references, inheritance, object lifetime, comes together into the one mechanism that makes a single line of code, shapes[i]->display();, correctly call a different function for every object in the array.

← Previous 14 - Inheritance