Welcome! If you’re just starting with C++, this article will make Function Overriding in C++ simple and practical. You’ll learn what it is, why it matters for polymorphism, and how to use keywords like virtual
, override
, and final
confidently. After every section, you’ll find “Try it yourself” challenges to practice.
Think of Function Overriding in C++ as letting a child class replace a parent’s behavior with a better, more specific version—while keeping the same interface. This powers runtime polymorphism and clean, extensible design.
🔹 What is Function Overriding in C++?
Function overriding happens when a derived class provides its own implementation of a virtual function defined in a base class, using the same function signature. Calls made via base pointers/references will execute the derived version at runtime (dynamic dispatch).
Key points:
- The base function must be marked
virtual
to enable runtime polymorphism. - The derived function must match the signature (name, parameters, constness, reference qualifiers). Return type can be covariant in some cases.
- Use the
override
keyword in derived classes for compile-time safety. - Calls through base references/pointers pick the most-derived override at runtime.
Try it yourself
- In your own words, explain the difference between redefining a function and overriding it.
- Write a simple base/derived pair with a virtual function and call it through a base pointer.
🔹 Basic Syntax and Rules
Mark the base function as virtual
, then implement the same signature in the derived class. Use override
to catch mistakes early.
#include <iostream>
using namespace std;
class Animal {
public:
// virtual enables runtime polymorphism
virtual void speak() const {
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
// override ensures this truly overrides a base virtual
void speak() const override {
cout << "Woof!\n";
}
};
int main() {
Animal a;
Dog d;
Animal& ref1 = a; // base reference
Animal& ref2 = d; // base reference to derived object
ref1.speak(); // Animal::speak
ref2.speak(); // Dog::speak due to overriding (dynamic dispatch)
}
Output
Animal sound
Woof!
Explanation: Because Animal::speak()
is virtual, the call dispatched via a base reference executes the derived override when the underlying object is a Dog
.
Try it yourself
- Create
Cat
overridingspeak()
with “Meow!”. - Store
Animal*
pointers toDog
andCat
in a vector and loop to callspeak()
.
🔹 Virtual Functions and Dynamic Dispatch
Overriding shines when you call methods through base references/pointers. C++ chooses the correct function implementation at runtime, based on the actual object type.
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Shape {
public:
virtual ~Shape() = default; // virtual destructor: important for polymorphic bases
virtual double area() const = 0; // pure virtual = must override
virtual const char* name() const { return "Shape"; }
};
class Circle : public Shape {
double r;
public:
explicit Circle(double radius) :
r(radius) {}
double area() const override
{ return 3.1415926535 * 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
Because calls go through Shape*
/Shape&
, the derived overrides run, producing correct runtime behavior for each shape.
Try it yourself
- Add
Triangle
with base and height. Overridearea()
andname()
. - Make
Shape::name()
pure virtual and ensure each derived type implements it.
🔹 Using override and final
override
ensures your function truly overrides a base virtual. final
prevents further overriding, helpful for design constraints and performance assumptions.
class Base {
public:
virtual void run(int x) { /* ... */ }
virtual void stop() { /* ... */ }
};
class Mid : public Base {
public:
void run(int x) override { /* correct signature, OK */ }
void stop() final override { /* cannot be overridden further */ }
};
class Leaf : public Mid {
public:
// void stop() override { } // ERROR: 'stop' is final in Mid
};
Tip: Always add override
on derived virtuals. The compiler will catch signature mismatches that would otherwise silently hide the base function.
Try it yourself
- Intentionally change the parameter type in
run
(e.g.,double
) and see the compile error withoverride
. - Mark a frequently used function
final
and observe that deeper classes cannot alter it.
🔹 Signature Matching, const, and Hiding
To override, the signature must match: same name, parameters, and qualifiers like const
. If it doesn’t, you get name hiding, not overriding. Use override
to prevent this.
#include <iostream>
using namespace std;
struct Base {
virtual void info() const {
cout << "Base::info\n";
}
};
struct BadDerived : Base {
// Missing const -> different signature; hides Base::info, doesn't override it
void info() {
cout << "BadDerived::info (no const)\n";
}
};
struct GoodDerived : Base {
void info() const override {
cout << "GoodDerived::info\n";
}
};
int main() {
Base* b1 = new BadDerived;
Base* b2 = new GoodDerived;
b1->info(); // calls Base::info (bad override failed)
b2->info(); // calls GoodDerived::info (correct)
delete b1;
delete b2;
}
Output
Base::info
GoodDerived::info
Notice how the missing const
prevented overriding and led to unexpected behavior. override
would have flagged this at compile time.
Try it yourself
- Add
const
toBadDerived::info
and mark itoverride
. Confirm the correct function runs. - Experiment with reference qualifiers (e.g.,
&
,&&
) and see how they affect overriding.
🔹 Covariant Return Types
C++ allows a derived override to return a more specific pointer/reference type than the base (covariance), as long as it remains type-safe.
struct Base {};
struct Derived : Base {};
struct MakerBase {
virtual Base* make() const {
return new Base();
}
};
struct MakerDerived : MakerBase {
// Covariant return: Derived* is allowed (more specific than Base*)
Derived* make() const override {
return new Derived();
}
};
This is handy for factory-like APIs that become more specific in derived classes while preserving the base interface.
Try it yourself
- Change the return type to an unrelated pointer type and see the compile error.
- Try covariance with references (e.g.,
Base&
→Derived&
).
🔹 Overriding vs Overloading
Overriding is a runtime polymorphism feature across base/derived classes with matching signatures. Overloading is compile-time resolution within the same scope based on different parameters.
Aspect | Overriding | Overloading |
---|---|---|
Purpose | Customize behavior in derived class | Provide multiple variants by parameters |
Where | Across base/derived classes | Within same class/scope |
Signature | Must match (except covariant return) | Must differ (params count/types) |
Keyword | Requires virtual in base; use override | No special keyword |
Dispatch | Runtime (dynamic) | Compile time (static) |
Try it yourself
- Create an
area()
overload set (different parameters) and a base/deriveddraw()
override pair. Observe when each one resolves.
🔹 Virtual Destructors and Default Arguments
Always make base destructors virtual
in polymorphic hierarchies to ensure proper cleanup. Remember that default arguments are bound at compile time to the static type, not overridden at runtime.
#include <iostream>
using namespace std;
struct Base {
virtual ~Base() {
cout << "Base dtor\n";
}
virtual void greet(const char* who = "Base") const {
cout << "Hello from " << who << "\n";
}
};
struct Derived : Base {
~Derived() override {
cout << "Derived dtor\n";
}
void greet(const char* who = "Derived") const override {
cout << "Hello from " << who << "\n";
}
};
int main() {
Base* p = new Derived();
p->greet(); // Calls Derived::greet but default arg from Base: prints "Base"
delete p; // Properly calls Derived then Base destructors
}
Output
Hello from Base
Derived dtor
Base dtor
Note: Even though the body is from Derived
, the default argument comes from the static type (Base*
). Avoid default arguments on virtuals to prevent confusion.
Try it yourself
- Remove
virtual
from the base destructor and delete aDerived*
throughBase*
— observe the wrong destructor call risk. - Call
greet("Custom")
to bypass default arguments and see correct behavior.
🔹 Pure Virtual Functions and Interfaces
Mark a function = 0
to make the class abstract (cannot instantiate it). Derived classes must override pure virtuals to be concrete.
struct ILogger {
virtual ~ILogger() = default;
virtual void log(const char* msg) = 0; // pure virtual
};
struct ConsoleLogger : ILogger {
void log(const char* msg) override {
std::cout << "[console] " << msg << "\n";
}
};
int main() {
// ILogger x; // ERROR: abstract
ConsoleLogger lg;
lg.log("Function Overriding in c++ is powerful!");
}
Output
[console] Function Overriding in c++ is powerful!
Try it yourself
- Add
FileLogger
(mock: print “[file] …”). StoreILogger*
to both and calllog
. - Make another pure virtual (e.g.,
flush()
) and implement it.
🔹 Best Practices and Common Pitfalls
- Mark base functions
virtual
and derived onesoverride
. - Make base destructors
virtual
in polymorphic hierarchies. - Avoid default arguments on virtual functions (bound statically).
- Match signatures exactly, including
const
and reference qualifiers. - Prefer interfaces (pure virtuals) for extensibility and testability.
- Use
final
to lock down critical overrides when needed.
Try it yourself
- Audit one of your class hierarchies. Add missing
override
and a virtual destructor if needed. - Refactor a switch-on-type design into virtual overrides.
🔹 Mini Project: Payments with Polymorphism
Let’s use Function Overriding in C++ to build an extensible payment system that chooses behavior at runtime based on payment method types.
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class PaymentMethod {
public:
virtual ~PaymentMethod() = default;
virtual const char* name() const { return "PaymentMethod"; }
virtual bool authorize(double amount) = 0; // pure virtual
virtual bool charge(double amount) = 0; // pure virtual
};
class CreditCard : public PaymentMethod {
public:
const char* name() const override { return "CreditCard"; }
bool authorize(double amount) override {
cout << "Checking credit limit for $"
<< amount << "\n";
return amount <= 1000; // trivial rule
}
bool charge(double amount) override {
cout << "Charging card: $"
<< amount << "\n";
return true;
}
};
class UPI : public PaymentMethod {
public:
const char* name() const override { return "UPI"; }
bool authorize(double amount) override {
cout << "Verifying UPI PIN for $"
<< amount << "\n";
return true;
}
bool charge(double amount) override {
cout << "Debiting UPI: $"
<< amount << "\n";
return amount <= 5000;
}
};
void checkout(PaymentMethod& pm, double amount) {
cout << "Using " << pm.name() << "\n";
if (pm.authorize(amount) && pm.charge(amount)) {
cout << "Payment successful!\n";
} else {
cout << "Payment failed.\n";
}
}
int main() {
vector<unique_ptr<PaymentMethod>> methods;
methods.push_back(make_unique<CreditCard>());
methods.push_back(make_unique<UPI>());
for (auto& m : methods) {
checkout(*m, 799.0); // same API, different behavior via overrides
}
}
Output
Using CreditCard
Checking credit limit for $799
Charging card: $799
Payment successful!
Using UPI
Verifying UPI PIN for $799
Debiting UPI: $799
Payment successful!
Try it yourself
- Add
Wallet
with balance tracking. Makecharge()
fail when balance is low. - Change
checkout
to retry with a backup method when the first fails.
🔹 FAQs about Function Overriding in C++
Q1: Do I need virtual
in the base to override?
Yes. Without virtual
, you don’t get runtime dispatch; you only get hiding. Try it: remove virtual
and see calls use the base version via base pointers.
Q2: What does override
do?
It asks the compiler to verify you are overriding a base virtual. If the signature doesn’t match, you get a helpful error. Try it: change a parameter type and watch the compiler complain.
Q3: What is final
used for?
final
prevents further overrides (on a function) or inheritance (on a class). Try it: mark a function final
and attempt to override it in a child; observe the error.
Q4: Why is a virtual destructor important?
Deleting derived objects through a base pointer requires a virtual destructor to call the correct destructors. Try it: remove virtual
and observe potential leaks/undefined behavior.
Q5: Can default arguments be overridden?
No. Default arguments are bound at compile time to the static type, not overridden dynamically. Try it: call a virtual with no arguments via a base pointer and see which default is used.
Q6: Is return type variance allowed?
Yes, covariant returns are allowed for pointers/references to classes in an inheritance chain. Try it: return Derived*
where the base returned Base*
.
Q7: Overriding vs overloading — what’s the difference?
Overriding changes behavior in derived classes with the same signature (runtime). Overloading defines multiple functions with different parameters in the same scope (compile time). Try it: create both and observe resolution.
Q8: Can access specifiers differ?
Yes, a derived override can change access (e.g., make a protected virtual public), but do so thoughtfully. Try it: expose a base protected virtual publicly in a derived class and review API impact.
Q9: Does overriding affect performance?
Virtual calls have a tiny overhead (vtable lookup) but are negligible for most apps. Try it: micro-benchmark direct calls vs virtual calls if you’re curious.
Q10: Can static functions be overridden?
No. Static functions are not virtual and belong to the class, not objects. Try it: mark a static function virtual — your compiler will reject it.
🔹 Wrapping Up
Function Overriding in C++ lets derived classes provide specialized behaviors behind a common interface, enabling clean polymorphic designs. Use virtual
in bases, override
in derived classes, and keep signatures consistent. Practice the “Try it yourself” tasks to master these patterns quickly. Happy coding!