Home C / C++ Virtual Functions in C++ Explained — Polymorphism, vtables and Real-World Patterns

Virtual Functions in C++ Explained — Polymorphism, vtables and Real-World Patterns

In Plain English 🔥
Imagine you manage a team of artists — a painter, a sculptor, and a musician. You tell each one: 'Go create something.' Every artist hears the same instruction, but each responds in their own way. You didn't need to know who was who — you just said 'create' and trusted them to do the right thing. Virtual functions work exactly like that: you call one method on a base pointer, and C++ figures out at runtime which derived class's version to actually run.
⚡ Quick Answer
Imagine you manage a team of artists — a painter, a sculptor, and a musician. You tell each one: 'Go create something.' Every artist hears the same instruction, but each responds in their own way. You didn't need to know who was who — you just said 'create' and trusted them to do the right thing. Virtual functions work exactly like that: you call one method on a base pointer, and C++ figures out at runtime which derived class's version to actually run.

Most C++ developers learn inheritance early on, wire up a few base and derived classes, and feel confident. Then they hit a wall: they call a method through a base-class pointer, expecting the derived class's behaviour, and the base class version runs instead. Nothing crashes. No warning. Just wrong results. That silent misbehaviour is one of the most disorienting bugs in C++, and it exists precisely because C++ defaults to static (compile-time) dispatch. Virtual functions are the switch that flips that to dynamic (runtime) dispatch.

The problem virtual functions solve is deceptively simple: how do you write one function that accepts any shape, any animal, any payment processor — without knowing at compile time which specific subtype it'll receive? Without virtual functions you end up with brittle if-else chains or switch statements that break every time you add a new type. With them, you write code against an abstraction once, and adding a new subtype requires zero changes to existing logic.

By the end of this article you'll understand exactly why virtual functions exist, how the vtable mechanism dispatches calls at runtime, how to design a clean polymorphic hierarchy, and — critically — the gotchas that silently corrupt your programs if you forget a destructor or misuse pure virtual functions. You'll also have a mental model for interview questions that catch even experienced developers off guard.

Static Dispatch vs Dynamic Dispatch — The Core Problem Virtual Functions Fix

C++ is a compiled language that loves to resolve things at compile time. When you call a method on a concrete object, the compiler knows exactly which function to jump to and bakes that address directly into the machine code. That's static dispatch — fast, zero overhead, resolved before the program even runs.

But polymorphism requires a different deal. You want to hold a pointer to a base class, point it at any derived object at runtime, and have the right method called automatically. The compiler can't know which derived type that pointer holds at compile time — it depends on data, user input, configuration files, network responses. So we need runtime dispatch.

Without the virtual keyword, C++ uses the declared type of the pointer, not the actual type of the object it points to. This means calling animal->speak() on a base Animal* that secretly points to a Dog will call Animal::speak(), ignoring Dog::speak() entirely. That's the bug virtual functions prevent.

Adding virtual tells the compiler: 'Don't hard-code this call. At runtime, look up the actual type of the object and call the right version.' The mechanism behind this lookup is the vtable — a behind-the-scenes pointer table every polymorphic class gets.

StaticVsDynamicDispatch.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
#include <iostream>
#include <string>

// --- WITHOUT virtual: static dispatch, wrong behaviour ---
class AnimalBase {
public:
    // No virtual keyword — compiler resolves this at compile time
    void speak() const {
        std::cout << "[AnimalBase] Some generic animal sound\n";
    }
};

class DogStatic : public AnimalBase {
public:
    // This OVERRIDES in the programmer's mind, but NOT polymorphically
    void speak() const {
        std::cout << "[DogStatic] Woof!\n";
    }
};

// --- WITH virtual: dynamic dispatch, correct behaviour ---
class Animal {
public:
    // virtual keyword tells C++: resolve this call at RUNTIME
    virtual void speak() const {
        std::cout << "[Animal] Some generic animal sound\n";
    }

    // Always virtual in a polymorphic base class (more on this later)
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    // override keyword is your safety net — compiler errors if signature doesn't match
    void speak() const override {
        std::cout << "[Dog] Woof!\n";
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "[Cat] Meow!\n";
    }
};

void makeAnimalSpeak(const Animal* animal) {
    // We don't know if this is a Dog, Cat, or something else — and we don't care
    animal->speak(); // vtable lookup happens HERE at runtime
}

int main() {
    std::cout << "=== Without virtual (static dispatch) ===\n";
    AnimalBase* staticAnimal = new DogStatic(); // pointer type is AnimalBase*
    staticAnimal->speak(); // calls AnimalBase::speak() — WRONG!
    delete staticAnimal;

    std::cout << "\n=== With virtual (dynamic dispatch) ===\n";
    Animal* dog = new Dog();
    Animal* cat = new Cat();

    makeAnimalSpeak(dog); // vtable says: call Dog::speak()
    makeAnimalSpeak(cat); // vtable says: call Cat::speak()

    delete dog;
    delete cat;

    return 0;
}
▶ Output
=== Without virtual (static dispatch) ===
[AnimalBase] Some generic animal sound

=== With virtual (dynamic dispatch) ===
[Dog] Woof!
[Cat] Meow!
⚠️
Watch Out:Forgetting `virtual` on a base class method is a completely silent bug. No compiler error, no runtime crash — just the wrong function running. Always use the `override` keyword on derived methods; if the base signature doesn't match, the compiler will catch it.

How the vtable Actually Works — What Happens Under the Hood

You don't need to manage the vtable manually — C++ does it automatically — but understanding it is the difference between guessing and knowing. Every class that declares or inherits at least one virtual function gets a vtable: a static array of function pointers, one entry per virtual method.

Every instance of that class carries a hidden pointer called the vptr (virtual pointer), slipped in by the compiler at the start of the object's memory layout. It costs one pointer's worth of memory (8 bytes on a 64-bit system). When you construct a Dog object, the constructor sets the vptr to point at Dog's vtable. When you construct a Cat, its vptr points at Cat's vtable.

When you call animal->speak() through a base pointer, C++ doesn't call any fixed address. Instead it follows the vptr to the vtable, reads the function pointer at the slot for speak(), and jumps there. That single extra indirection is the entire cost of virtual dispatch — one pointer dereference at call time.

This is why virtual functions have a small performance overhead compared to non-virtual calls, but for the vast majority of programs that overhead is utterly negligible. The design flexibility you gain is almost always worth it.

VtableLayout.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
#include <iostream>

// Demonstrates vptr memory overhead and vtable-driven dispatch
class Shape {
public:
    virtual double area() const {
        return 0.0;
    }

    virtual double perimeter() const {
        return 0.0;
    }

    virtual void describe() const {
        std::cout << "I am a generic shape\n";
    }

    virtual ~Shape() = default;
};

class Circle : public Shape {
private:
    double radius;
public:
    explicit Circle(double r) : radius(r) {}

    // Each override replaces the corresponding vtable SLOT for Circle's vtable
    double area() const override {
        return 3.14159265 * radius * radius;
    }

    double perimeter() const override {
        return 2.0 * 3.14159265 * radius;
    }

    void describe() const override {
        std::cout << "I am a Circle with radius " << radius << "\n";
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }

    double perimeter() const override {
        return 2.0 * (width + height);
    }

    void describe() const override {
        std::cout << "I am a Rectangle " << width << "x" << height << "\n";
    }
};

// This function works for ANY Shape subtype — written once, open to extension
void printShapeInfo(const Shape& shape) {
    shape.describe();                  // resolved via vptr at runtime
    std::cout << "  Area      : " << shape.area() << "\n";
    std::cout << "  Perimeter : " << shape.perimeter() << "\n";
}

int main() {
    // sizeof shows the vptr overhead: Shape is not empty even with no data members
    std::cout << "sizeof(Shape)     = " << sizeof(Shape) << " bytes\n";
    std::cout << "sizeof(Circle)    = " << sizeof(Circle) << " bytes\n";
    std::cout << "sizeof(Rectangle) = " << sizeof(Rectangle) << " bytes\n\n";

    Circle    bigCircle(7.0);
    Rectangle door(0.9, 2.1);

    printShapeInfo(bigCircle);  // vptr points to Circle's vtable
    std::cout << "\n";
    printShapeInfo(door);       // vptr points to Rectangle's vtable

    return 0;
}
▶ Output
sizeof(Shape) = 8 bytes
sizeof(Circle) = 16 bytes
sizeof(Rectangle) = 24 bytes

I am a Circle with radius 7
Area : 153.938
Perimeter : 43.9823

I am a Rectangle 0.9x2.1
Area : 1.89
Perimeter : 6
🔥
The Cost Is Real But Tiny:The vtable adds one pointer per object (8 bytes on 64-bit) and one extra memory indirection per virtual call. In tight loops over millions of objects this can matter — but for UI layers, game entities, payment processors, or anything I/O-bound, it's completely irrelevant. Profile before optimising.

Pure Virtual Functions and Abstract Classes — Enforcing a Contract

Sometimes a base class method doesn't have any sensible default implementation. What would a generic Shape::area() even return? It's a placeholder, a promise that every subclass must fulfil. That's exactly what a pure virtual function expresses.

Declare a pure virtual function by appending = 0 to the declaration. The moment a class has even one pure virtual function, it becomes an abstract class and can't be instantiated directly. This is a feature, not a limitation — it forces every concrete subclass to implement the contract or the code won't compile.

Think of an abstract class as an interface with optional partial implementation. It's the foundation of the Open/Closed Principle: your code is open to new types (just add another subclass) but closed to modification (you never touch the calling code again).

Abstract base classes also serve as documentation. When a new developer joins the team and sees a pure virtual processPayment() in PaymentGateway, they immediately understand: 'I have to implement this — there's no fallback.' That intent is clearer than any comment.

PaymentGatewayPolymorphism.cpp · CPP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
#include <iostream>
#include <string>
#include <vector>
#include <memory>

// Abstract base class — models the CONCEPT of a payment gateway
// Cannot be instantiated: new PaymentGateway() would be a compile error
class PaymentGateway {
public:
    // Pure virtual — every concrete gateway MUST implement this
    virtual bool processPayment(double amountUSD) = 0;

    // Pure virtual — must be implemented, returns gateway name for logging
    virtual std::string gatewayName() const = 0;

    // Non-pure virtual with shared logic all gateways inherit
    virtual void logTransaction(double amountUSD, bool success) const {
        std::cout << "[" << gatewayName() << "] "
                  << (success ? "SUCCESS" : "FAILED")
                  << " — $" << amountUSD << "\n";
    }

    // Virtual destructor is MANDATORY in any polymorphic base class
    virtual ~PaymentGateway() = default;
};

class StripeGateway : public PaymentGateway {
private:
    std::string apiKey;
public:
    explicit StripeGateway(const std::string& key) : apiKey(key) {}

    bool processPayment(double amountUSD) override {
        // Real code would make an HTTP call here
        bool success = (amountUSD < 10000.0); // simulate limit
        logTransaction(amountUSD, success);   // calls shared base implementation
        return success;
    }

    std::string gatewayName() const override {
        return "Stripe";
    }
};

class PayPalGateway : public PaymentGateway {
private:
    std::string merchantId;
public:
    explicit PayPalGateway(const std::string& id) : merchantId(id) {}

    bool processPayment(double amountUSD) override {
        bool success = (amountUSD < 5000.0); // PayPal has tighter limit here
        logTransaction(amountUSD, success);
        return success;
    }

    std::string gatewayName() const override {
        return "PayPal";
    }
};

// This billing engine knows NOTHING about Stripe or PayPal — only PaymentGateway
// Adding a new gateway (Braintree, Square) requires zero changes here
void runBillingCycle(PaymentGateway& gateway, const std::vector<double>& charges) {
    std::cout << "--- Billing cycle via " << gateway.gatewayName() << " ---\n";
    for (double charge : charges) {
        gateway.processPayment(charge);
    }
    std::cout << "\n";
}

int main() {
    // unique_ptr handles memory automatically — no manual delete needed
    auto stripe = std::make_unique<StripeGateway>("sk_live_abc123");
    auto paypal = std::make_unique<PayPalGateway>("merchant_xyz");

    std::vector<double> monthlyCharges = {29.99, 199.00, 6500.00};

    runBillingCycle(*stripe, monthlyCharges);
    runBillingCycle(*paypal, monthlyCharges);

    return 0;
}
▶ Output
--- Billing cycle via Stripe ---
[Stripe] SUCCESS — $29.99
[Stripe] SUCCESS — $199
[Stripe] FAILED — $6500

--- Billing cycle via PayPal ---
[PayPal] SUCCESS — $29.99
[PayPal] SUCCESS — $199
[PayPal] FAILED — $6500
⚠️
Pro Tip:Pure virtual functions CAN have a body — it just can't be called via the vtable. You can call it explicitly as `PaymentGateway::processPayment(amount)` from a derived class. This pattern lets you provide a default fallback that derived classes opt into intentionally, rather than inheriting silently.

The Virtual Destructor Rule — The Mistake That Causes Silent Memory Leaks

Here's the rule: if a class has ANY virtual function, its destructor must also be virtual. Period. This is the most common virtual function mistake, and it causes real memory leaks in production code.

Here's why: when you delete a derived object through a base-class pointer, C++ calls the destructor via the pointer's declared type — which is the base. If the base destructor isn't virtual, Dog::~Dog() never runs. Any resources the Dog allocated (file handles, heap memory, database connections) are never released. The object is partially destroyed.

Marking the base destructor virtual means the vtable handles destructor dispatch exactly like any other virtual call: it calls the most-derived destructor first, which then chains upward through the hierarchy automatically. Every resource gets cleaned up.

The quick rule: if you ever write delete basePointer; and basePointer might point to a derived object, your base destructor must be virtual. If you're using std::unique_ptr or std::shared_ptr for polymorphic ownership — which you should — the same rule applies.

VirtualDestructorDemo.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
#include <iostream>

// === BROKEN: non-virtual destructor — memory leak waiting to happen ===
class SensorBase {
public:
    SensorBase()  { std::cout << "[SensorBase] constructed\n"; }
    // BUG: non-virtual destructor — derived destructor will NOT be called
    ~SensorBase() { std::cout << "[SensorBase] destroyed\n"; }

    virtual void readData() const {
        std::cout << "[SensorBase] reading...\n";
    }
};

class TemperatureSensor : public SensorBase {
private:
    int* calibrationBuffer; // simulates heap resource
public:
    TemperatureSensor() {
        calibrationBuffer = new int[256]; // allocates memory
        std::cout << "[TemperatureSensor] constructed, buffer allocated\n";
    }
    // This NEVER runs if deleted through a SensorBase* (non-virtual base destructor)
    ~TemperatureSensor() {
        delete[] calibrationBuffer;       // memory freed here — but only if called!
        std::cout << "[TemperatureSensor] destroyed, buffer freed\n";
    }
    void readData() const override {
        std::cout << "[TemperatureSensor] 23.4 degrees Celsius\n";
    }
};

// === FIXED: virtual destructor — correct cleanup guaranteed ===
class DeviceBase {
public:
    DeviceBase()  { std::cout << "[DeviceBase] constructed\n"; }
    virtual ~DeviceBase() { std::cout << "[DeviceBase] destroyed\n"; } // FIXED

    virtual void activate() const {
        std::cout << "[DeviceBase] activating...\n";
    }
};

class GPSDevice : public DeviceBase {
private:
    double* waypointCache;
public:
    GPSDevice() {
        waypointCache = new double[512];
        std::cout << "[GPSDevice] constructed, waypoint cache allocated\n";
    }
    ~GPSDevice() override {
        delete[] waypointCache; // this WILL run because DeviceBase destructor is virtual
        std::cout << "[GPSDevice] destroyed, waypoint cache freed\n";
    }
    void activate() const override {
        std::cout << "[GPSDevice] GPS lock acquired\n";
    }
};

int main() {
    std::cout << "=== BROKEN (non-virtual destructor) ===\n";
    SensorBase* brokenSensor = new TemperatureSensor();
    brokenSensor->readData();
    delete brokenSensor; // TemperatureSensor::~TemperatureSensor() is SKIPPED — leak!

    std::cout << "\n=== FIXED (virtual destructor) ===\n";
    DeviceBase* gps = new GPSDevice();
    gps->activate();
    delete gps; // GPSDevice::~GPSDevice() runs FIRST, then DeviceBase::~DeviceBase()

    return 0;
}
▶ Output
=== BROKEN (non-virtual destructor) ===
[SensorBase] constructed
[TemperatureSensor] constructed, buffer allocated
[TemperatureSensor] 23.4 degrees Celsius
[SensorBase] destroyed

=== FIXED (virtual destructor) ===
[DeviceBase] constructed
[GPSDevice] constructed, waypoint cache allocated
[GPSDevice] GPS lock acquired
[GPSDevice] destroyed, waypoint cache freed
[DeviceBase] destroyed
⚠️
Watch Out:Notice the broken example: `[TemperatureSensor] destroyed, buffer freed` never prints. That's your memory leak happening in real time. Compilers like GCC and Clang will warn about this with `-Wnon-virtual-dtor`. Enable warnings — they're telling you something important.
AspectNon-Virtual FunctionVirtual Function
Dispatch timingCompile time (static)Runtime (dynamic)
Resolved byPointer/reference declared typeActual object type via vtable
PerformanceZero overhead — direct callOne pointer dereference per call
Memory cost per objectNoneOne vptr (8 bytes on 64-bit)
Can be overridden polymorphicallyNo — hides, does not overrideYes — proper override via vtable
Use `override` keywordHas no effect (but harmless)Strongly recommended — compile-time safety
Abstract variantN/APure virtual (= 0) enforces subclass contract
Destructor recommendationOnly if class is not polymorphicAlways virtual in any polymorphic base class

🎯 Key Takeaways

  • The virtual keyword changes dispatch from compile-time (pointer type) to runtime (actual object type via vtable) — without it, overrides in derived classes are silently ignored when calling through a base pointer.
  • Every class with at least one virtual function gets a hidden vptr (8 bytes) per instance pointing to its vtable — this is the entire runtime cost of polymorphism, and for most applications it's negligible.
  • Any polymorphic base class MUST have a virtual destructor — a non-virtual destructor causes the derived destructor to be silently skipped when deleting through a base pointer, leaking every resource the derived object owns.
  • Pure virtual functions (= 0) make a class abstract, prevent direct instantiation, and serve as a compile-time-enforced contract that all concrete subclasses must fulfil — use them to design extensible systems that never need to touch existing calling code.

⚠ Common Mistakes to Avoid

  • Mistake 1: Forgetting virtual on the base destructor — Symptom: derived class destructor silently never runs; heap resources are leaked, file handles stay open, but the program doesn't crash so it's invisible in short tests. Fix: add virtual ~BaseClass() = default; to every class that has any virtual method. Enable -Wnon-virtual-dtor on your compiler to catch this automatically.
  • Mistake 2: Not using the override keyword on derived methods — Symptom: you misspell the method name or get the const-qualifier wrong (e.g., void speak() instead of void speak() const), and instead of overriding you silently create a new unrelated method. The base version keeps running. Fix: always write override on every method that intends to override a virtual. The compiler will immediately error if the signature doesn't match anything in the base class.
  • Mistake 3: Calling a virtual function from a constructor or destructor — Symptom: you expect the derived class's override to run during base class construction, but it doesn't — the object is only partially constructed and its vptr still points to the base vtable. The base version runs instead. Fix: never call virtual functions in constructors or destructors. Use a factory method or a post-construction init() call if you need polymorphic initialisation behaviour.

Interview Questions on This Topic

  • QWhat is the vtable and how does C++ use it to implement virtual function dispatch at runtime?
  • QWhy must the destructor of a polymorphic base class be virtual, and what exactly happens at runtime if it isn't?
  • QCan a constructor be virtual in C++? Why or why not — and what happens when you call a virtual function from inside a constructor?

Frequently Asked Questions

What is the difference between a virtual function and a pure virtual function in C++?

A virtual function has a body in the base class and provides a default implementation that derived classes may optionally override. A pure virtual function (declared with = 0) has no required implementation in the base class, makes the class abstract (non-instantiable), and forces every concrete subclass to provide its own implementation or it won't compile.

Does using virtual functions slow down my C++ program?

In practice, very rarely. Each virtual call costs one extra pointer dereference through the vtable, and each object carries one hidden vptr (8 bytes on 64-bit systems). This is measurable only in extremely tight loops over millions of objects with trivial call bodies. For typical application code — UI, networking, game entities, business logic — the overhead is completely negligible compared to I/O, memory allocation, or algorithmic complexity.

Can you call a virtual function from a constructor in C++?

You can call it syntactically, but it won't behave polymorphically — and that's the trap. During base class construction, the object's vptr points to the BASE class vtable because the derived part hasn't been constructed yet. So the base version of the virtual function runs, not the derived version you might expect. Avoid calling virtual functions in constructors or destructors for this reason.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousInline Functions in C++Next →SFINAE in C++
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged