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)
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
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.
override catches a signature mismatch at compile time
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
Missing virtual destructor: the derived destructor never runs
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
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.
}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.