Welcome! If you’re just starting with C++, this guide will make Virtual Function in C++ easy to understand and apply. We’ll use clear explanations, commented code, expected outputs, and short “Try it yourself” challenges after each section so you can practice as you learn.
Think of a Virtual Function in C++ like a universal remote: the button (function name) stays the same, but the actual action depends on the device (the real object type). This enables runtime polymorphism—calling the most appropriate function based on the object you have.
🔹 What is a Virtual Function in C++?
A virtual function is a member function declared with the virtual
keyword in a base class. When you call it through a base class pointer or reference, C++ dynamically dispatches to the most-derived override at runtime. This is the foundation of runtime polymorphism.
Key points to remember:
- Declare virtuals in the base class; derived classes override them with the same signature.
- Call via base references or pointers to get dynamic dispatch.
- Use
override
in derived classes to catch mistakes at compile time. - Make base destructors
virtual
in polymorphic hierarchies.
Try it yourself
- In your own words, describe why calling through a base pointer/reference is important for dynamic dispatch.
- Write a tiny base/derived pair where the base has a virtual function and the derived overrides it.
🔹 Basic Syntax: virtual, override, and Dynamic Dispatch
Mark the base function as virtual
. In the derived class, match the signature and add override
for safety. Calling via a base reference/pointer invokes the derived version at runtime.
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Animal {
public:
virtual ~Animal() = default; // virtual destructor for safe cleanup
virtual void speak() const { // virtual enables dynamic dispatch
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() const override { // override checks the signature at compile time
cout << "Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() const override {
cout << "Meow!\n";
}
};
int main() {
vector<unique_ptr<Animal>> zoo;
zoo.push_back(make_unique<Dog>());
zoo.push_back(make_unique<Cat>());
for (const auto& a : zoo) {
a->speak(); // Calls Dog::speak or Cat::speak at runtime
}
}
Output
Woof!
Meow!
Explanation: Even though the variable type is Animal*
, the actual object type (Dog or Cat) determines which speak()
runs. That’s dynamic dispatch via a virtual function in C++.
Try it yourself
- Add a
Cow
class overridingspeak()
with “Moo!”. - Push it into the vector and confirm the correct sound is printed.
🔹 Pure Virtual Functions and Abstract Classes
Use a pure virtual function (= 0
) to make a class abstract. Abstract classes cannot be instantiated and serve as interfaces that derived classes must implement.
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0; // pure virtual: must be overridden
virtual const char* name() const = 0; // pure virtual: interface requirement
};
class Circle : public Shape {
double r;
public:
explicit Circle(double radius) :
r(radius) {}
double area() const override
{ return 3.141592653589793 * r * r; }
const char* name() const override
{ return "Circle"; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w_, double h_) :
w(w_), h(h_) {}
double area() const override
{ return w * h; }
const char* name() const override
{ return "Rectangle"; }
};
int main() {
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(3.0));
shapes.push_back(make_unique<Rectangle>(4.0, 5.0));
for (const auto& s : shapes) {
cout << s->name() << " area: "
<< s->area() << "\n";
}
}
Output
Circle area: 28.2743
Rectangle area: 20
Explanation: Shape
defines the interface via pure virtuals. Derived classes must implement these functions to be instantiable, using virtual function in C++ to customize behavior.
Try it yourself
- Create
Triangle
with base and height. Implementarea()
andname()
. - Push it into the vector and print all areas.
🔹 override and final: Safer Overrides
override
asks the compiler to verify that you are actually overriding a virtual function. final
prevents further overriding of a function (or further inheritance if applied to a class).
class Base {
public:
virtual void run(int x) { /* base logic */ }
virtual void stop() { /* base stop */ }
};
class Mid : public Base {
public:
void run(int x) override
{ /* OK: exact match */ }
void stop() final override
{ /* cannot be overridden further */ }
};
class Leaf : public Mid {
public:
// void stop() override { } // ERROR: 'stop' is final in Mid
};
Tip: Always write override
on derived virtuals. It prevents subtle bugs from signature mismatches (e.g., missing const
or different parameter types).
Try it yourself
- Change
run(int)
inMid
torun(double)
and see the compile-time error withoverride
. - Remove
final
onstop()
and add a new override inLeaf
.
🔹Virtual Destructors (Must-Have for Polymorphism)
If you delete a derived object through a base pointer, the base class must have a virtual destructor to ensure the derived destructor runs first. Without it, you risk leaks or undefined behavior.
#include <iostream>
using namespace std;
struct Base {
// virtual ensures correct cleanup
virtual ~Base() { cout << "Base dtor\n"; }
virtual void greet() const { cout << "Base\n"; }
};
struct Derived : Base {
~Derived() override { cout << "Derived dtor\n"; }
void greet() const override { cout << "Derived\n"; }
};
int main() {
Base* p = new Derived();
p->greet(); // prints "Derived"
delete p; // calls ~Derived then ~Base (correct order)
}
Output
Derived
Derived dtor
Base dtor
Explanation: Virtual destructors are essential anytime you plan to delete derived objects through base pointers. They’re the safety net for polymorphic classes using a virtual function in C++.
Try it yourself
- Remove
virtual
fromBase
destructor and observe that onlyBase dtor
runs (danger!). Revert your change after testing.
🔹 Signature Matching, const, and Name Hiding
To override, the signature must match exactly (name, parameters, qualifiers like const
). If not, you’ll get name hiding instead of overriding. override
helps catch mistakes early.
#include <iostream>
using namespace std;
struct B {
virtual void info() const {
cout << "B::info\n";
}
};
struct Bad : B {
// Missing const -> different signature; hides B::info instead of overriding
void info() {
cout << "Bad::info (no const)\n";
}
};
struct Good : B {
void info() const override {
cout << "Good::info\n";
}
};
int main() {
B* p1 = new Bad;
B* p2 = new Good;
p1->info(); // calls B::info (not overridden)
p2->info(); // calls Good::info (overridden)
delete p1;
delete p2;
}
Output
B::info
Good::info
Explanation: The missing const
caused a different signature, so the base version ran. Use override
to catch this mistake immediately.
Try it yourself
- Add
const
toBad::info
and mark itoverride
. Confirm the derived version runs.
🔹 Default Arguments with Virtual Functions
Default arguments are bound at compile time to the static type, not at runtime. A virtual call uses the derived body but the default argument from the static type of the expression.
#include <iostream>
using namespace std;
struct Base {
virtual ~Base() = default;
virtual void greet(const char* who = "Base") const {
cout << "Hello from " << who << "\n";
}
};
struct Derived : Base {
void greet(const char* who = "Derived") const override {
cout << "Hello from " << who << "\n";
}
};
int main() {
Base* p = new Derived();
p->greet(); // Calls Derived::greet body, but uses Base's default argument
p->greet("Custom"); // Explicit argument bypasses the default
delete p;
}
Output
Hello from Base
Hello from Custom
Guideline: Avoid default arguments on virtuals to prevent surprises. Prefer overloads or explicit arguments instead when using a virtual function in C++.
Try it yourself
- Remove defaults and provide an overload
greet()
that forwards with the desired name.
🔹 Covariant Return Types
Overrides may return a more specific pointer/reference type than the base (covariant return), as long as it’s in the same inheritance chain. This keeps interfaces flexible yet type-safe.
struct Base {};
struct Derived : Base {};
struct MakerBase {
virtual Base* make() const {
return new Base();
}
};
struct MakerDerived : MakerBase {
Derived* make() const override {
return new Derived();
} // covariant return
};
Try it yourself
- Try changing the derived return type to an unrelated class pointer and watch the compiler reject it.
- Repeat the example with references (
Base&
→Derived&
).
🔹 Under the Hood: vtable and vptr
Most compilers implement virtual calls using a hidden pointer in each polymorphic object (the vptr) that points to a table of function addresses (the vtable). When you call a virtual, the program looks up the correct function through this table. This lookup is fast, but slightly slower than a non-virtual call.
Takeaway: Virtual dispatch is a tiny, acceptable overhead for the flexibility you gain from a virtual function in C++.
Try it yourself
- Create a tight loop calling a virtual function and compare timing to a non-virtual call (use
chrono
). Observe the small difference.
🔹 Multiple Inheritance and Virtual Functions
In multiple inheritance, a class can implement virtual functions from multiple bases. Just ensure signatures match and use override
to keep things safe.
#include <iostream>
using namespace std;
struct Speaker {
virtual ~Speaker() = default;
virtual void say() const { cout << "Speaker\n"; }
};
struct Greeter {
virtual ~Greeter() = default;
virtual void greet() const { cout << "Hello\n"; }
};
struct Host : Speaker, Greeter {
void say() const override { cout << "Host speaking\n"; }
void greet() const override { cout << "Welcome!\n"; }
};
int main() {
Host h;
Speaker* s = &h;
Greeter* g = &h;
s->say(); // Host::say
g->greet(); // Host::greet
}
Output
Host speaking
Welcome!
Try it yourself
- Add another base with a virtual function and override it in
Host
. Confirm calls through each base pointer dispatch correctly.
🔹 Performance and Best Practices
- Use
override
on all derived virtuals; it prevents silent bugs. - Make base destructors
virtual
when used polymorphically. - Prefer pure virtual interfaces for extensible designs.
- Avoid default arguments on virtual functions.
- Don’t overuse virtuals where simple non-virtual functions suffice.
- Document intended polymorphic use in comments and tests.
Try it yourself
- Refactor a switch-on-type block into a polymorphic hierarchy using a virtual function in C++.
- Add unit tests calling through base pointers to ensure correct overrides run.
🔹 Mini Project: Notifications with Virtual Function in C++
Let’s build a pluggable notification system. Each notifier implements the same interface but behaves differently. Calls go through base references, and virtual dispatch picks the right behavior at runtime.
#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;
class Notifier {
public:
virtual ~Notifier() = default;
virtual const char* name() const { return "Notifier"; }
virtual void send(const string& msg) = 0; // pure virtual
};
class EmailNotifier : public Notifier {
public:
const char* name() const override { return "Email"; }
void send(const string& msg) override {
cout << "[Email] " << msg << "\n";
}
};
class SMSNotifier : public Notifier {
public:
const char* name() const override { return "SMS"; }
void send(const string& msg) override {
cout << "[SMS] " << msg << "\n";
}
};
class PushNotifier : public Notifier {
public:
const char* name() const override { return "Push"; }
void send(const string& msg) override {
cout << "[Push] " << msg << "\n";
}
};
void broadcast(Notifier& n, const string& msg) {
cout << "Using " << n.name() << ": ";
n.send(msg); // virtual call -> derived behavior
}
int main() {
vector<unique_ptr<Notifier>> list;
list.push_back(make_unique<EmailNotifier>());
list.push_back(make_unique<SMSNotifier>());
list.push_back(make_unique<PushNotifier>());
for (auto& n : list)
broadcast(*n, "Hello via Virtual Function in C++!");
}
Output
Using Email: [Email] Hello via Virtual Function in C++!
Using SMS: [SMS] Hello via Virtual Function in C++!
Using Push: [Push] Hello via Virtual Function in C++!
Try it yourself
- Add a
SlackNotifier
and include it in the loop. - Change
broadcast
to prepend timestamps without modifying notifier subclasses.
🔹 FAQs about Virtual Function in C++
Q1: Do I need virtual
in the base to get overriding?
Yes. Without virtual
, calls through base pointers/references won’t dispatch to derived implementations; you’ll get static binding and potential name hiding.
Q2: What does the override
keyword do?
It tells the compiler to verify that your function overrides a base virtual. If the signature doesn’t match, compilation fails with a helpful message.
Q3: Why are virtual destructors important?
They ensure the correct destructor runs when deleting derived objects through base pointers, preventing leaks and undefined behavior.
Q4: Can default arguments be “overridden”?
No. Default arguments are bound to the static type at compile time. The virtual body is chosen at runtime, but the default comes from the static type.
Q5: Are virtual calls slow?
They have a tiny overhead (an indirect call via the vtable). For most applications, this cost is negligible compared to the design benefits.
Q6: Can static functions be virtual?
No. Static member functions are not tied to objects and cannot be virtual.
Q7: What about covariant return types?
Allowed for pointer/reference returns where the derived returns a type that’s a subclass of the base’s return type (e.g., Base*
→ Derived*
).
Q8: Does access level have to match?
No. A derived override can widen access (e.g., protected → public), but design carefully as it changes the class’s public surface.
Q9: Overriding vs overloading—what’s the difference?
Overriding replaces base behavior in derived classes (runtime). Overloading provides multiple functions with the same name but different parameters (compile time).
Q10: Can a class be final to block inheritance?
Yes. Mark the class with final
to prevent further derivation; useful when you want to lock down hierarchies.
🔹 Wrapping Up
A Virtual Function in C++ enables clean, flexible polymorphic designs by choosing behavior at runtime. Use virtual
in base classes, override
in derived classes, provide a virtual destructor, and avoid default arguments on virtuals. Practice with the “Try it yourself” tasks to master these patterns quickly. Happy coding!