Bytebook Compiler

Inheritance & Polymorphism

Virtual Functions & Runtime Polymorphism

Base-class pointers to derived objects, why virtual changes which function runs, override, virtual destructors, and how the vtable makes it work.

5 min read4 runnable examples4-question quiz

The problem: static binding

A base-class pointer can point to a derived object. But by default, calling a function through it runs the base version — the compiler decides at compile time from the pointer's type. This is static (early) binding.

C++
#include <iostream>
using namespace std;

class Animal {
public:
    void speak() const { cout << "..." << endl; }          // NOT virtual
};
class Dog : public Animal {
public:
    void speak() const { cout << "Woof!" << endl; }
};

int main() {
    Dog d;
    Animal *p = &d;      // base pointer, derived object
    d.speak();           // Woof!
    p->speak();          // "..." — chosen by the pointer type
    return 0;
}
Output
Woof!
...

The fix: virtual functions

Mark the base function virtual. Now the function is chosen at run time from the actual object — dynamic (late) binding. This is runtime polymorphism.

C++
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Employee {
protected:
    string name;
    double base;
public:
    Employee(const string &n, double b) : name(n), base(b) {}
    virtual double pay() const { return base; }
    virtual string role() const { return "Employee"; }
    virtual ~Employee() {}
    void payslip() const { cout << name << " (" << role() << "): Rs " << pay() << endl; }
};

class Manager : public Employee {
    double bonus;
public:
    Manager(const string &n, double b, double bo) : Employee(n, b), bonus(bo) {}
    double pay() const override { return base + bonus; }
    string role() const override { return "Manager"; }
};

class Intern : public Employee {
public:
    Intern(const string &n) : Employee(n, 15000) {}
    string role() const override { return "Intern"; }       // keeps Employee::pay
};

int main() {
    Manager m("Priya", 90000, 25000);
    Intern i("Rahul");
    Employee e("Sanjay", 60000);
    vector<const Employee *> staff = {&e, &m, &i};
    for (const Employee *s : staff) s->payslip();          // one loop, three behaviours
    return 0;
}
Output
Sanjay (Employee): Rs 60000
Priya (Manager): Rs 115000
Rahul (Intern): Rs 15000

payslip() is written once in the base class, yet it calls the right pay() and role() for each object. Adding a new kind of employee requires no change to payslip — that is the real power of polymorphism.

Virtual destructors

If you delete a derived object through a base pointer and the base destructor is not virtual, only the base destructor runs — the derived part leaks.

C++
#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() { cout << "~Base" << endl; }     // remove 'virtual' and ~Derived is skipped
};
class Derived : public Base {
    int *buffer = new int[1000];
public:
    ~Derived() { delete[] buffer; cout << "~Derived (freed buffer)" << endl; }
};

int main() {
    Base *p = new Derived;
    delete p;            // runs ~Derived, then ~Base
    return 0;
}
Output
~Derived (freed buffer)
~Base

How it works: the vtable

For each class with virtual functions, the compiler builds a virtual table (vtable): an array of pointers to that class's versions of the virtual functions. Each object gets a hidden pointer (the vptr) to its class's vtable. A virtual call follows object → vptr → vtable → function. That is one extra pointer lookup per call, and one extra pointer per object:

C++
#include <iostream>
using namespace std;

class Plain   { int x = 0; public: int f() { return x; } };
class Virtual { int x = 0; public: virtual int f() { return x; } };

int main() {
    cout << "sizeof(Plain)   = " << sizeof(Plain) << endl;
    cout << "sizeof(Virtual) = " << sizeof(Virtual) << " (int + hidden vptr, padded)" << endl;
    return 0;
}
Output
sizeof(Plain)   = 4
sizeof(Virtual) = 16 (int + hidden vptr, padded)
Static binding Dynamic binding
decided at compile time decided at run time
normal and overloaded functions virtual functions called through pointers/references
faster tiny overhead (vtable lookup)
also called early binding also called late binding

Test yourself

4 questions · pick an answer to check it instantly

  1. 1

    What decides which function runs when a virtual function is called through a base pointer?

  2. 2

    What happens if a base class destructor is not virtual and you delete a derived object through a base pointer?

  3. 3

    What is a vtable?

  4. 4

    What does override do?