Virtual Functions
Master virtual functions to enable runtime polymorphism and dynamic dispatch
Learn how virtual functions enable runtime polymorphism, allowing base class pointers to call the correct derived class methods dynamically.
A Simple Example
#include <iostream>
#include <string>
#include <vector>
class Shape {
protected:
std::string name;
public:
Shape(std::string name) : name{name} {
}
virtual void draw() const {
std::cout << "Drawing a generic shape: " << name << "\n";
}
virtual double area() const {
return 0.0;
}
virtual ~Shape() {
std::cout << "Shape destructor: " << name << "\n";
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(std::string name, double r) : Shape{name}, radius{r} {
}
void draw() const override {
std::cout << "Drawing circle: " << name << " with radius " << radius << "\n";
}
double area() const override {
return 3.14159 * radius * radius;
}
~Circle() override {
std::cout << "Circle destructor: " << name << "\n";
}
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(std::string name, double w, double h)
: Shape{name}, width{w}, height{h} {
}
void draw() const override {
std::cout << "Drawing rectangle: " << name << " (" << width << "x" << height << ")" << "\n";
}
double area() const override {
return width * height;
}
~Rectangle() override {
std::cout << "Rectangle destructor: " << name << "\n";
}
};
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle{"Circle1", 5.0});
shapes.push_back(new Rectangle{"Rect1", 4.0, 6.0});
shapes.push_back(new Circle{"Circle2", 3.0});
std::cout << "Drawing all shapes:" << "\n";
for (const Shape* shape : shapes) {
shape->draw(); // Calls correct derived class method!
std::cout << "Area: " << shape->area() << "\n";
}
std::cout << "\nCleaning up:" << "\n";
for (Shape* shape : shapes) {
delete shape; // Calls correct derived class destructor!
}
return 0;
}
Breaking It Down
The virtual Keyword
- What it does: Marks a function for dynamic dispatch - calls resolved at runtime, not compile-time
- Without virtual: Base class pointer calls base version even if object is derived type
- With virtual: Calls the correct derived class version based on actual object type
- Remember: Virtual functions enable true polymorphism in C++
The override Keyword
- What it does: Explicitly marks a function as overriding a base class virtual function
- Benefit: Compiler catches typos and signature mismatches (e.g., missing const)
- Not required but strongly recommended for safety and clarity
- Remember: Override catches bugs at compile-time instead of runtime
Virtual Destructors
- Why critical: Without virtual destructor, derived destructors never called when deleting via base pointer
- Consequence: Memory leaks and resources not released properly
- Rule: Always make destructors virtual in base classes with virtual functions
- Remember: Virtual destructor prevents object slicing and ensures proper cleanup
How Virtual Functions Work (vtable)
- Behind the scenes: Each object with virtual functions contains a hidden vptr (virtual pointer)
- The vptr points to a vtable (virtual function table) with function addresses
- Runtime cost: One extra pointer dereference per virtual call
- Memory cost: One pointer per object, one vtable per class
Why This Matters
- Virtual functions are the key to true polymorphism in C++.
- They let you write code that works with base class pointers but automatically calls the right derived class function at runtime.
- This is how you build extensible systems - add new derived classes without changing existing code.
- It's the foundation of plugin systems, game engines, UI frameworks, and countless other applications.
- Understanding virtual functions transforms you from someone who uses classes to someone who designs flexible, maintainable systems.
Critical Insight
Virtual functions work through a hidden pointer (vptr) in each object that points to a virtual function table (vtable) containing function addresses. This is why virtual functions have a tiny runtime cost - an extra pointer dereference. But without virtual, C++ would call the wrong function!
class Base {
public:
void nonVirtual() { std::cout << "Base nonVirtual\n"; }
virtual void virt() { std::cout << "Base virtual\n"; }
};
class Derived : public Base {
public:
void nonVirtual() { std::cout << "Derived nonVirtual\n"; }
void virt() override { std::cout << "Derived virtual\n"; }
};
Base* ptr = new Derived{};
ptr->nonVirtual(); // Calls Base version - wrong!
ptr->virt(); // Calls Derived version - correct!
// Virtual = runtime type, non-virtual = compile-time type
Best Practices
Always use virtual destructors: If a class has virtual functions, make the destructor virtual to ensure proper cleanup when deleting through base pointers.
Use override keyword: Always mark overriding functions with override to catch signature mismatches at compile-time.
Prefer smart pointers: Use std::unique_ptr<Shape> or std::shared_ptr<Shape> instead of raw pointers to avoid manual memory management.
Mark final if no further derivation: Use final keyword on functions or classes that should not be overridden or inherited from.
Common Mistakes
Forgetting Virtual Destructor: Without a virtual destructor, deleting through a base pointer won't call derived destructors, causing memory leaks.
Not Using override Keyword: Omitting override can hide bugs where you think you're overriding but you're not.
Calling Virtual Functions in Constructors: During construction, the object hasn't become the derived type yet, so virtual calls resolve to the base version.
Slicing Objects: Assigning derived object to base class value (not pointer) slices away derived parts.
Debug Challenge
This Shape hierarchy has a bug - when deleting shapes through base pointers, the derived destructors are never called. Click the highlighted line to fix it:
Quick Quiz
- What does the
virtualkeyword enable?
- Why must destructors be virtual in base classes?
- What does the
overridekeyword do?
Practice Playground
Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.
Output:
Error:
Lesson Progress
- Fix This Code
- Quick Quiz
- Practice Playground - run once