CppVirtual Functions

Virtual Functions

Marking a member function virtual tells the compiler to resolve calls to it using the object's actual, runtime type instead of the declared (static) type of the pointer or reference used to call it. This is the mechanism that makes runtime polymorphism work in C++.

Without virtual vs. with virtual

Without virtual: static binding (surprising to many beginners)

CPP
class Animal {
public:
    void speak() const { std::cout << "...\n"; } // NOT virtual
};

class Dog : public Animal {
public:
    void speak() const { std::cout << "Woof!\n"; }
};

int main() {
    Dog d;
    Animal* ptr = &d;

    ptr->speak(); // Prints "..." — resolved using Animal (the pointer's static type)
    d.speak();    // Prints "Woof!" — called directly on a Dog, no ambiguity
}

With virtual: dynamic dispatch

CPP
class Animal {
public:
    virtual void speak() const { std::cout << "...\n"; } // virtual
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "Woof!\n"; }
};

int main() {
    Dog d;
    Animal* ptr = &d;

    ptr->speak(); // Prints "Woof!" — resolved using Dog (the object's actual type)
}

The only difference between the two examples is the virtual keyword on Animal::speak, and it completely changes the behavior of ptr->speak().

How it works: the vtable, conceptually

Compilers typically implement virtual functions using a virtual function table (vtable) — a hidden, per-class array of function pointers, one entry per virtual function. Every object of a class with at least one virtual function carries a hidden pointer to its class's vtable. A virtual call looks up the function through that table at runtime instead of jumping directly to a compile-time-known address, which is what allows the correct derived-class version to be found even when only a base-class pointer is visible at the call site. (This is an implementation detail — the standard doesn't mandate vtables — but it's how essentially every real compiler does it, and it explains why virtual calls have a small runtime cost compared to non-virtual ones.)

The override keyword

override (C++11) is placed after a member function's parameter list to declare “this is meant to override a virtual function from a base class.” The compiler then verifies that a matching virtual function actually exists in the base class.

Always use override
Without `override`, a small typo in the signature — a missing `const`, a mismatched parameter type — silently creates a brand-new, unrelated function instead of overriding the base one. The code still compiles, but polymorphism quietly stops working, and calls through a base pointer keep running the base class's version. `override` turns that silent, hard-to-debug mistake into an immediate compile error.

override catches a signature mismatch at compile time

CPP
class Animal {
public:
    virtual void speak(int volume) const { std::cout << "...\n"; }
};

class Dog : public Animal {
public:
    // Typo: takes no parameter, so this does NOT override speak(int) above.
    // Without 'override', this compiles silently as an unrelated new function.
    void speak() const override { std::cout << "Woof!\n"; }
    // Error: 'speak' marked 'override' but does not override any base class methods
};
Virtual destructors
A base class with virtual functions almost always needs a virtual destructor
If you delete a derived object through a **base-class pointer** and the base class's destructor is not `virtual`, only the base class's destructor runs — the derived class's destructor is skipped entirely. Any resources the derived class owns are leaked, and this is undefined behavior in the general case.

Missing virtual destructor: the derived destructor never runs

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

class Derived : public Base {
public:
    int* data = new int[100];
    ~Derived() {
        std::cout << "Derived destroyed\n";
        delete[] data;
    }
};

int main() {
    Base* b = new Derived();
    delete b;
    // Prints only "Base destroyed" — Derived's destructor never runs.
    // 'data' is leaked, and this is technically undefined behavior.
}

Fixed: a virtual destructor

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

class Derived : public Base {
public:
    int* data = new int[100];
    ~Derived() override {
        std::cout << "Derived destroyed\n";
        delete[] data;
    }
};

int main() {
    Base* b = new Derived();
    delete b;
    // Prints "Derived destroyed" then "Base destroyed" — correct cleanup order.
}
Note
Rule of thumb: as soon as a class has *any* virtual function, give it a virtual destructor too (even if the destructor's body is empty). This guarantees derived objects are destroyed correctly no matter what kind of pointer they're deleted through.
What's next
  • A pure virtual function (= 0) takes this idea further, defining a method with no default implementation at all — the subject of Abstract Classes, next.