Menü schliessen
Created: February 20th 2026
Last updated: February 20th 2026
Categories: IT Development
Author: Ian Walser

Virtual Functions in C++ Explained: What They Do and How They Really Work Under the Hood

What Are Virtual Functions in C++? A Beginner-Friendly Deep Dive

If you've been learning C++ for a while, you've probably stumbled across the keyword virtual and wondered what the fuss is about. Virtual functions are one of the cornerstones of object-oriented programming in C++, enabling a powerful concept called runtime polymorphism. In plain terms: they let you write flexible code where the right function gets called automatically, even when you're working through a base class pointer. In this post, we'll break down what virtual functions do, why you need them, and - most excitingly - how C++ actually makes them work under the hood using a mechanism called the vtable.


The Problem Virtual Functions Solve

Let's start with a concrete problem. Imagine you're building a simple game with different types of enemies. Each enemy has an attack() method, but each one attacks differently. Without virtual functions, C++ decides at compile time which function to call - based on the type of the pointer, not the actual object. This is called static dispatch, and it causes a nasty surprise:

#include <iostream>

class Enemy {
public:
    void attack() {
        std::cout << "Enemy attacks!" << std::endl;
    }
};

class Goblin : public Enemy {
public:
    void attack() {
        std::cout << "Goblin stabs you!" << std::endl;
    }
};

int main() {
    Enemy* e = new Goblin();
    e->attack(); // What prints here?
    return 0;
}

You might expect "Goblin stabs you!" - but you'll get "Enemy attacks!" instead. The compiler looks at the type of the pointer (Enemy*) and calls Enemy::attack(), completely ignoring the fact that the actual object is a Goblin. This is the problem virtual functions were born to fix.


Introducing the virtual Keyword

The fix is simple: add the virtual keyword to the base class method. This tells the compiler: "Don't decide which function to call at compile time - wait until runtime and check what the actual object type is." This is called dynamic dispatch.

#include <iostream>

class Enemy {
public:
    virtual void attack() {
        std::cout << "Enemy attacks!" << std::endl;
    }
};

class Goblin : public Enemy {
public:
    void attack() override {
        std::cout << "Goblin stabs you!" << std::endl;
    }
};

class Dragon : public Enemy {
public:
    void attack() override {
        std::cout << "Dragon breathes fire!" << std::endl;
    }
};

int main() {
    Enemy* e1 = new Goblin();
    Enemy* e2 = new Dragon();

    e1->attack(); // "Goblin stabs you!"
    e2->attack(); // "Dragon breathes fire!"

    delete e1;
    delete e2;
    return 0;
}

Notice the override keyword in the derived classes. It's not strictly required, but it's considered best practice because it tells the compiler you intend to override a virtual function - so it can warn you if you accidentally get the signature wrong.


How Virtual Functions Work Under the Hood: The vtable

This is where things get really interesting. How does C++ know at runtime which function to call? The answer is a mechanism called the virtual function table, or vtable.

The vtable: A Hidden Lookup Table

When you declare a class with at least one virtual function, the C++ compiler automatically creates a vtable for that class. A vtable is essentially an array of function pointers - one entry per virtual function in the class. Each derived class that overrides any virtual functions gets its own vtable, with its overridden function pointers swapped in.

Here's a conceptual illustration of what the compiler generates behind the scenes for our example above:

// Conceptual - not real C++ code, just to illustrate

// vtable for Enemy:
// [0] -> &Enemy::attack

// vtable for Goblin:
// [0] -> &Goblin::attack   (overridden)

// vtable for Dragon:
// [0] -> &Dragon::attack   (overridden)

The vptr: The Hidden Pointer Inside Every Object

The vtable alone isn't enough - the program also needs to know which vtable to use for a given object. So the compiler secretly adds a hidden pointer to every object of a class with virtual functions. This pointer is called the vptr (virtual pointer). It's set during object construction to point to the correct vtable for that object's type.

So when you write e1->attack(), C++ doesn't just call a function directly. Instead it does roughly this at runtime:

// What e1->attack() actually translates to (conceptually):
// 1. Follow e1's vptr to find the vtable
// 2. Look up the function pointer at the correct index (index 0 for attack())
// 3. Call that function

(*e1->vptr[0])(e1); // Simplified pseudocode

This is a tiny bit slower than a regular function call because of the extra pointer indirection. In most applications this is completely negligible, but it's good to be aware of in performance-critical hot paths.


Pure Virtual Functions and Abstract Classes

Sometimes you want to define a base class that forces every derived class to implement a function. For this, C++ gives us pure virtual functions. You declare them by assigning = 0 to the virtual function:

#include <iostream>

class Shape {
public:
    virtual double area() const = 0; // Pure virtual function
    virtual ~Shape() {}
};

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

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

int main() {
    Shape* s1 = new Circle(5.0);
    Shape* s2 = new Rectangle(4.0, 6.0);

    std::cout << "Circle area: " << s1->area() << std::endl;
    std::cout << "Rectangle area: " << s2->area() << std::endl;

    delete s1;
    delete s2;
    return 0;
}

A class with at least one pure virtual function becomes an abstract class. You cannot instantiate it directly - Shape* s = new Shape(); would be a compile error. This is a great way to define interfaces in C++.


The Virtual Destructor: Don't Forget This!

This is one of the most common mistakes junior developers make with virtual functions. If you're deleting a derived class object through a base class pointer, you must make the base class destructor virtual. Otherwise, only the base class destructor runs - and you get a memory leak.

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

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

int main() {
    Base* obj = new Derived();
    delete obj; // Both destructors run correctly
    return 0;
}

Without virtual on the base destructor, only ~Base() would run when you call delete obj, leaking any resources held by Derived. The rule of thumb: if a class has any virtual function, give it a virtual destructor.


Key Takeaways: Virtual Functions in C++ at a Glance

  • Virtual functions enable runtime polymorphism - the correct function is chosen based on the actual object type, not the pointer type.
  • Under the hood, the compiler creates a vtable (array of function pointers) for each class with virtual functions.
  • Every object with virtual functions carries a hidden vptr that points to its class's vtable.
  • Virtual function calls have a tiny overhead due to the extra pointer indirection - usually irrelevant, but worth knowing.
  • Pure virtual functions (= 0) create abstract classes and enforce implementation in derived classes.
  • Always declare your base class destructor as virtual when using polymorphism to avoid memory leaks.
  • Use the override keyword in derived classes for safer, more readable code.

Conclusion

Virtual functions might seem like a small syntactic detail, but they're the engine behind one of C++'s most powerful features: runtime polymorphism. Now that you understand not just how to use them but why they work the way they do - thanks to the vtable and vptr - you're equipped to write cleaner, more extensible C++ code and to reason confidently about what's happening at the machine level. Keep experimenting with inheritance hierarchies, abstract interfaces, and polymorphic containers, and these concepts will quickly become second nature.