Skip to content
  • Virtual Functions & vtables · C++

Virtual Functions & vtables

LearnViaNotes @learnvianotes· 4 min read

A virtual function is dispatched on the object's dynamic type at run time — the mechanism behind runtime polymorphism.

Static vs dynamic binding

C++
#include <iostream>
#include <memory>
#include <vector>

struct Shape {
    virtual double area() const { return 0; }
    std::string kind() const { return "shape"; }        // non-virtual
    virtual ~Shape() = default;                         // virtual destructor!
};
struct Rect : Shape {
    double w, h;
    Rect(double a, double b) : w(a), h(b) {}
    double area() const override { return w * h; }     // `override`: compiler checks it overrides
    std::string kind() const { return "rect"; }         // hides, doesn't override
};
struct Circle : Shape {
    double r;
    explicit Circle(double x) : r(x) {}
    double area() const override { return 3.14 * r * r; }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Rect>(2, 3));
    shapes.push_back(std::make_unique<Circle>(1));
    for (const auto& s : shapes) std::cout << s->kind() << ' ' << s->area() << '\n';

    Rect r(1, 1);
    Shape& ref = r;
    std::cout << ref.area() << ' ' << ref.kind() << ' ' << r.kind() << '\n';
}
Output
shape 6
shape 3.14
1 shape rect
Call through…Non-virtual functionVirtual function
Object r.f()Static typeStatic type (= dynamic)
Pointer/reference to base p->f()Static type (Shape::f)Dynamic type (Rect::f)

How it works: vptr & vtable

  1. vtable per class

    Each class with virtual functions gets one static table of function pointers, one slot per virtual function.

  2. vptr per object

    Every object carries a hidden pointer (usually the first 8 bytes) to its class's vtable. Constructors set it.

  3. Call

    p->area() compiles to: load p->vptr, load slot k, call it — one or two extra memory loads.

C++
#include <iostream>
struct Plain   { int x; void f() {} };
struct Virtual { int x; virtual void f() {} virtual ~Virtual() = default; };
int main() {
    std::cout << sizeof(Plain) << ' ' << sizeof(Virtual) << '\n';   // 4 vs 16: vptr (8) + int + padding
}
Output
4 16

override and final

C++
struct Base {
    virtual void save(int version) const {}
    virtual ~Base() = default;
};
struct Derived : Base {
    void save(long version) const override {}   // error: doesn't override (parameter type differs)
};
int main() {}
C++
#include <iostream>
struct Base { virtual void run() const { std::cout << "base\n"; } virtual ~Base() = default; };
struct Leaf final : Base {                       // no class may derive from Leaf
    void run() const override { std::cout << "leaf\n"; }
};
struct Mid : Base { void run() const final { std::cout << "mid\n"; } };   // run can't be overridden further
int main() {
    const Base& b = Leaf{};
    b.run();
    Mid{}.run();
}
Output
leaf
mid

Virtual destructors

C++ — missing virtual destructor
#include <iostream>
struct Base {
    virtual void speak() const {}
    ~Base() { std::cout << "~Base\n"; }                        // NOT virtual
};
struct Derived : Base {
    int* data = new int[100];
    ~Derived() { delete[] data; std::cout << "~Derived\n"; }
};
int main() {
    Base* p = new Derived;
    delete p;          // undefined behaviour: only ~Base would run → ~Derived skipped, leak
}
Compiler error (-Wall)
error: delete called on non-final 'Base' that has virtual functions but non-virtual destructor [-Werror,-Wdelete-non-abstract-non-virtual-dtor]

Object slicing

C++
#include <iostream>
struct Animal { virtual const char* sound() const { return "..."; } virtual ~Animal() = default; };
struct Cat : Animal { const char* sound() const override { return "meow"; } };
void byValue(Animal a)            { std::cout << a.sound() << '\n'; }   // copies only the Animal part
void byRef(const Animal& a)       { std::cout << a.sound() << '\n'; }
int main() {
    Cat c;
    byValue(c);
    byRef(c);
}
Output
...
meow

Calling virtuals in constructors

C++
#include <iostream>
struct Base {
    Base() { hello(); }                          // dispatches to Base::hello, not Derived's!
    virtual void hello() const { std::cout << "Base::hello\n"; }
    virtual ~Base() = default;
};
struct Derived : Base { void hello() const override { std::cout << "Derived::hello\n"; } };
int main() { Derived d; d.hello(); }
Output
Base::hello
Derived::hello

How are virtual functions implemented?

Why is calling a virtual function from a constructor dangerous?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report