C++ Virtual Functions

A virtual function is a member function in a base class that you expect to be redefined in derived classes. When you call a virtual function through a base class pointer or reference, C++ decides at runtime which version to run based on the actual type of the object, not the type of the pointer. This behavior is called runtime polymorphism, and it is one of the core mechanisms that makes object-oriented design in C++ powerful — it lets you write code that works with a general base type while automatically getting specialized behavior for each derived type.

Overview / How it works

By default, C++ function calls are resolved using static binding (also called early binding): the compiler looks at the declared type of the variable or pointer and picks the matching function at compile time. This works fine when you call functions directly on objects, but it causes a problem when you work with base class pointers or references that actually point to derived objects — the compiler would always call the base class version, even if the object is really a derived type.

Declaring a function virtual tells the compiler to use dynamic binding (late binding) instead: the decision of which function to call is deferred until the program is actually running, based on the real, dynamic type of the object. This is exactly what you want when you use inheritance to model “is-a” relationships and want each subclass to provide its own implementation of shared behavior.

What happens internally

Most C++ compilers implement virtual functions using a mechanism called a virtual table, or vtable. When a class declares or inherits at least one virtual function, the compiler generates a hidden static array of function pointers for that class — one entry per virtual function. Every object of that class then carries a hidden pointer, often called the vptr, that points to its class’s vtable. This pointer is set automatically inside the constructor.

When you call a virtual function through a pointer or reference, the compiler generates code that: (1) follows the object’s vptr to find its vtable, (2) looks up the correct function pointer at a fixed offset, and (3) calls through that pointer. Because the vptr always points at the vtable that matches the object’s actual type, the correct override is invoked even though the calling code only knows about the base class. This lookup is one extra pointer indirection compared to a normal function call, which is why virtual calls have a small, usually negligible, runtime cost compared to non-virtual calls.

Constructors cannot be virtual (the object’s type isn’t fully established until construction completes), but destructors can and, in most cases involving inheritance, should be virtual — more on that in Common Mistakes below.

Syntax

class Base {
public:
    virtual ReturnType functionName(Parameters);
};

class Derived : public Base {
public:
    ReturnType functionName(Parameters) override;
};
  • virtual — keyword used in the base class to mark a function as overridable and enable dynamic dispatch.
  • override — optional but strongly recommended keyword in the derived class; it tells the compiler “this must match a virtual function in the base class,” and produces a compile error if the signature doesn’t actually match.
  • = 0 — when written after a virtual function’s parameter list, makes it a pure virtual function, meaning the base class provides no implementation and the class becomes abstract (it cannot be instantiated directly).
  • A function only needs the virtual keyword once in the base class; derived classes automatically inherit the virtual behavior even if they omit the keyword (though using override is best practice).

Examples

Example 1: Without virtual — the problem

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "The animal makes a sound." << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "The dog barks." << endl;
    }
};

int main() {
    Dog myDog;
    Animal* ptr = &myDog;
    ptr->speak();
    return 0;
}

Output:

The animal makes a sound.

Even though ptr actually points to a Dog object, it is declared as an Animal*, so without virtual the compiler resolves speak() at compile time using the pointer’s static type. The derived class’s version is completely ignored — a common source of confusion for beginners using inheritance.

Example 2: With virtual — the fix

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {
        cout << "The animal makes a sound." << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "The dog barks." << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "The cat meows." << endl;
    }
};

int main() {
    Animal* animals[3];
    Dog myDog;
    Cat myCat;
    Animal generic;

    animals[0] = &myDog;
    animals[1] = &myCat;
    animals[2] = &generic;

    for (int i = 0; i < 3; i++) {
        animals[i]->speak();
    }
    return 0;
}

Output:

The dog barks.
The cat meows.
The animal makes a sound.

Now each call to speak() through the Animal* array resolves to the correct override at runtime, based on each object’s actual type. This is the essence of polymorphism: one line of calling code, animals[i]->speak();, produces different behavior depending on what object is actually stored there.

Example 3: Pure virtual functions and an abstract base class

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

class Shape {
public:
    virtual double area() const = 0;
    virtual string name() const = 0;
    virtual ~Shape() {
        cout << "Destroying a shape." << endl;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        const double PI = 3.14159265358979;
        return PI * radius * radius;
    }
    string name() const override {
        return "Circle";
    }
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override {
        return width * height;
    }
    string name() const override {
        return "Rectangle";
    }
};

int main() {
    vector<Shape*> shapes;
    shapes.push_back(new Circle(3.0));
    shapes.push_back(new Rectangle(4.0, 5.0));

    for (Shape* s : shapes) {
        cout << s->name() << " area: " << s->area() << endl;
    }

    for (Shape* s : shapes) {
        delete s;
    }
    return 0;
}

Output:

Circle area: 28.2743
Rectangle area: 20
Destroying a shape.
Destroying a shape.

Shape declares area() and name() as pure virtual (= 0), so it has no implementation of its own and cannot be instantiated — you can never write Shape s;. Instead, it acts purely as an interface that Circle and Rectangle must implement. A vector<Shape*> can hold pointers to any concrete subclass, and the loop calls the correct area() and name() for each one without knowing the concrete type in advance. Note the virtual destructor, too — it guarantees that delete s; correctly cleans up whichever derived object s actually points to.

How it works step by step

  • The compiler sees at least one virtual function in Shape, so it generates a vtable for Shape, one for Circle, and one for Rectangle, each populated with pointers to the correct overrides.
  • When a Circle object is constructed, its hidden vptr is set to point at Circle‘s vtable (this happens automatically, before your constructor body even runs).
  • The line s->area() does not jump directly to a function; it follows s‘s vptr to the object’s vtable, reads the entry for area(), and calls through that pointer.
  • Because the vptr was set based on the object’s real type (Circle or Rectangle), the correct override always runs, regardless of the fact that s is declared as Shape*.
  • The same mechanism applies to the virtual destructor: delete s; looks up the destructor through the vtable, ensuring the derived class’s destructor (and then the base’s) runs, instead of only the base’s.

Common Mistakes

Mistake 1: Forgetting a virtual destructor

#include <iostream>
using namespace std;

class Base {
public:
    ~Base() {
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() {
        data = new int[10];
        cout << "Derived constructor" << endl;
    }
    ~Derived() {
        delete[] data;
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;
    return 0;
}

Output:

Derived constructor
Base destructor

Notice “Derived destructor” never prints. Because ~Base() is not virtual, delete ptr; resolves statically to Base‘s destructor only — Derived‘s destructor is skipped, so the dynamically allocated data array is never freed. This is technically undefined behavior according to the C++ standard, and it is one of the most common real-world bugs involving inheritance. The fix is simple: whenever a class has virtual functions or is meant to be used polymorphically through base pointers, give it a virtual ~Base() { ... } destructor.

Mistake 2: A signature mismatch silently breaks overriding

Without the override keyword, a small typo in parameter types creates an unrelated overload instead of an override, and the base version still gets called through base pointers — a bug that can be very hard to spot. Using override turns this into an immediate compile-time error:

class Base {
public:
    virtual void process(int value) {
        cout << "Base::process(int)" << endl;
    }
};

class Derived : public Base {
public:
    void process(double value) override {
        cout << "Derived::process(double)" << endl;
    }
};

This fails to compile with an error like “process marked override, but does not override a base class member,” because process(double) does not match process(int). That compile error is exactly what you want — it catches the mistake immediately instead of letting it silently produce the wrong behavior at runtime. Always add override to every function you intend as an override.

Best Practices

  • Always declare a virtual destructor in any class that has other virtual functions or that is designed to be a polymorphic base class.
  • Mark every overriding function with override so the compiler verifies the signature matches and catches typos immediately.
  • Use pure virtual functions (= 0) to define interfaces — classes that exist purely to specify what derived classes must implement.
  • Prefer passing and storing objects through base class pointers or references (e.g. Shape*, Shape&) when you want polymorphic behavior; passing by value causes “slicing,” where the derived part of the object is cut off.
  • Consider marking a class or function final if you want to prevent further overriding or inheritance, which can also help the compiler optimize away virtual dispatch.
  • Don’t make every function virtual by default — virtual dispatch has a small runtime cost and prevents some compiler optimizations like inlining, so reserve it for functions genuinely meant to be customized by subclasses.

Practice Exercises

  • Create a base class Employee with a virtual function double calculateBonus() that returns a base amount. Derive Manager and Developer classes that override it with different formulas, then store several employees in a vector<Employee*> and print each one’s bonus.
  • Write an abstract class Instrument with a pure virtual function void play(). Derive Piano and Guitar classes, each printing a different message. Try to instantiate Instrument directly and observe the compile error.
  • Take the “missing virtual destructor” example from this lesson, add a virtual keyword to ~Base(), and predict the new output before running it. Confirm that “Derived destructor” now prints.

Summary

  • A virtual function enables dynamic (runtime) binding, so calls through a base class pointer or reference resolve to the actual object’s type.
  • Internally, this is implemented with a per-class vtable of function pointers and a per-object vptr that selects the right table.
  • A pure virtual function (= 0) makes a class abstract, meaning it can’t be instantiated and must be subclassed to be used.
  • Always give polymorphic base classes a virtual destructor to avoid resource leaks and undefined behavior.
  • Always mark overriding functions with override to let the compiler catch signature mismatches at compile time.