C++ Virtual Functions: Complete Guide with Examples
C++ Virtual Functions: Complete Guide with Examples
Virtual functions are a cornerstone of C++ polymorphism, enabling runtime binding and allowing derived classes to override base class behavior. This guide covers all aspects of virtual functions with practical examples.
What are Virtual Functions?
A virtual function is a member function declared in a base class that can be overridden in derived classes. When called through a base class pointer or reference, the actual function called is determined at runtime based on the object’s type.
Basic Example
#include <iostream>
class Base {
public:
virtual void display() {
std::cout << "Base class display()" << std::endl;
}
void nonVirtual() {
std::cout << "Base class nonVirtual()" << std::endl;
}
};
class Derived : public Base {
public:
void display() override {
std::cout << "Derived class display()" << std::endl;
}
void nonVirtual() {
std::cout << "Derived class nonVirtual()" << std::endl;
}
};
int main() {
Derived derived;
Base* basePtr = &derived;
// Virtual function: runtime binding
basePtr->display(); // Output: Derived class display()
// Non-virtual function: compile-time binding
basePtr->nonVirtual(); // Output: Base class nonVirtual()
return 0;
}
How Virtual Functions Work: VTable
C++ implements virtual functions using a virtual function table (vtable):
- Each class with virtual functions has a vtable
- The vtable contains pointers to virtual functions
- Objects contain a pointer to their class’s vtable
- Function calls are resolved through the vtable at runtime
VTable Structure
class Base {
public:
virtual void func1() { }
virtual void func2() { }
int data;
};
class Derived : public Base {
public:
void func1() override { }
virtual void func3() { }
int moreData;
};
Base class vtable:
[0] -> Base::func1()
[1] -> Base::func2()
Derived class vtable:
[0] -> Derived::func1() // Overridden
[1] -> Base::func2() // Inherited
[2] -> Derived::func3() // New virtual function
Virtual Function Syntax
Declaring Virtual Functions
class Base {
public:
// Virtual function declaration
virtual void func();
// Pure virtual function (abstract)
virtual void pureFunc() = 0;
// Virtual destructor
virtual ~Base();
};
Overriding Virtual Functions
class Derived : public Base {
public:
// Override virtual function
void func() override;
// Override pure virtual function
void pureFunc() override;
// Destructor
~Derived() override;
};
Runtime Polymorphism
Virtual functions enable runtime polymorphism (late binding), where the function to call is determined at runtime based on the actual object type.
Example: Runtime Binding
#include <iostream>
#include <vector>
class Animal {
public:
virtual void makeSound() {
std::cout << "Some animal sound" << std::endl;
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Woof! Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Meow! Meow!" << std::endl;
}
};
int main() {
std::vector<Animal*> animals;
animals.push_back(new Dog());
animals.push_back(new Cat());
animals.push_back(new Dog());
// Runtime polymorphism: correct function called for each object
for (Animal* animal : animals) {
animal->makeSound(); // Calls appropriate derived class function
}
// Cleanup
for (Animal* animal : animals) {
delete animal;
}
return 0;
}
Virtual Function Rules and Constraints
1. Virtual Functions Must Have Same Signature
class Base {
public:
virtual void func(int x) { }
};
class Derived : public Base {
public:
// ✅ Correct: same signature
void func(int x) override { }
// ❌ Wrong: different signature (hiding, not overriding)
void func(double x) { }
};
2. Covariant Return Types
Derived classes can return a more derived type when overriding virtual functions.
class Base {
public:
virtual Base* clone() {
return new Base(*this);
}
};
class Derived : public Base {
public:
// ✅ Covariant return type
Derived* clone() override {
return new Derived(*this);
}
};
3. Virtual Functions Can Be Private
class Base {
private:
virtual void internalFunc() {
std::cout << "Base::internalFunc()" << std::endl;
}
public:
void callInternal() {
internalFunc(); // Can call private virtual function
}
};
class Derived : public Base {
private:
void internalFunc() override {
std::cout << "Derived::internalFunc()" << std::endl;
}
};
int main() {
Derived derived;
Base* basePtr = &derived;
basePtr->callInternal(); // Output: Derived::internalFunc()
return 0;
}
4. Virtual Functions and Access Specifiers
class Base {
public:
virtual void func() { }
};
class Derived : public Base {
private:
void func() override { } // ⚠️ Allowed but not recommended
};
Pure Virtual Functions
A pure virtual function is a virtual function with no implementation in the base class. Classes with pure virtual functions are abstract classes and cannot be instantiated.
Syntax
class AbstractBase {
public:
// Pure virtual function
virtual void pureFunc() = 0;
// Regular virtual function
virtual void regularFunc() {
std::cout << "Default implementation" << std::endl;
}
virtual ~AbstractBase() = default;
};
Example: Abstract Shape Class
#include <iostream>
#include <cmath>
class Shape {
public:
// Pure virtual function - must be overridden
virtual double area() const = 0;
// Pure virtual function
virtual double perimeter() const = 0;
// Regular virtual function with default implementation
virtual void draw() const {
std::cout << "Drawing shape" << std::endl;
}
virtual ~Shape() = default;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) { }
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
void draw() const override {
std::cout << "Drawing circle (radius: " << radius << ")" << std::endl;
}
};
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;
}
double perimeter() const override {
return 2 * (width + height);
}
void draw() const override {
std::cout << "Drawing rectangle (" << width << "x" << height << ")" << std::endl;
}
};
int main() {
// Shape shape; // ❌ Error: cannot instantiate abstract class
Circle circle(5.0);
Rectangle rect(4.0, 6.0);
Shape* shapes[] = {&circle, &rect};
for (Shape* shape : shapes) {
shape->draw();
std::cout << "Area: " << shape->area() << std::endl;
std::cout << "Perimeter: " << shape->perimeter() << std::endl;
}
return 0;
}
Virtual Destructors
When using polymorphism with base class pointers, the base class must have a virtual destructor to ensure proper cleanup of derived class objects.
Without Virtual Destructor (Problem)
class Base {
public:
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) { }
~Derived() {
delete[] data;
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // ❌ Only Base destructor called! Memory leak!
return 0;
}
With Virtual Destructor (Solution)
class Base {
public:
virtual ~Base() { // ✅ Virtual destructor
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) { }
~Derived() override {
delete[] data;
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // ✅ Both destructors called correctly
return 0;
}
Performance Considerations
Overhead of Virtual Functions
- VTable lookup: One extra indirection per virtual function call
- Memory overhead: Each object stores a vtable pointer (typically 8 bytes on 64-bit)
- Cache effects: VTable lookups can cause cache misses
When to Use Virtual Functions
✅ Use virtual functions when:
- You need runtime polymorphism
- Base class defines interface, derived classes provide implementations
- You’re using base class pointers/references to derived objects
❌ Avoid virtual functions when:
- Performance is critical and polymorphism isn’t needed
- Functions are never overridden
- Template-based polymorphism (CRTP) is more appropriate
Performance Example
// Non-virtual: faster, compile-time binding
class FastBase {
public:
void func() { } // Direct call, no overhead
};
// Virtual: slower, runtime binding
class SlowBase {
public:
virtual void func() { } // VTable lookup required
};
Advanced Topics
Virtual Function Calls in Constructors/Destructors
Virtual function calls in constructors and destructors call the function for the current class being constructed/destructed, not the most derived class.
class Base {
public:
Base() {
func(); // Calls Base::func(), not Derived::func()
}
virtual void func() {
std::cout << "Base::func()" << std::endl;
}
virtual ~Base() {
func(); // Calls Base::func()
}
};
class Derived : public Base {
public:
Derived() {
func(); // Calls Derived::func()
}
void func() override {
std::cout << "Derived::func()" << std::endl;
}
~Derived() {
func(); // Calls Derived::func()
}
};
int main() {
Derived d;
// Output:
// Base::func() (in Base constructor)
// Derived::func() (in Derived constructor)
// Derived::func() (in Derived destructor)
// Base::func() (in Base destructor)
return 0;
}
Final Virtual Functions (C++11)
The final keyword prevents a virtual function from being overridden in derived classes.
class Base {
public:
virtual void func() { }
};
class Derived : public Base {
public:
void func() override final { } // Cannot be overridden further
};
class FurtherDerived : public Derived {
public:
// ❌ Error: cannot override final function
// void func() override { }
};
Best Practices
- Always use virtual destructors in base classes when using polymorphism
- Use
overridekeyword (C++11+) to catch errors at compile-time - Use
finalkeyword when you want to prevent further overriding - Prefer pure virtual functions for abstract interfaces
- Document virtual functions that are meant to be overridden
- Avoid virtual functions in performance-critical code paths when not needed
- Don’t call virtual functions in constructors/destructors expecting derived behavior
Summary
Virtual functions are essential for runtime polymorphism in C++:
- Virtual functions enable runtime binding through vtables
- Pure virtual functions create abstract classes
- Virtual destructors are required for proper cleanup with polymorphism
- Use
overridekeyword for compile-time checking - Understand performance implications of virtual function overhead
- Follow best practices for effective use of virtual functions
Mastering virtual functions is crucial for effective object-oriented design in C++.