Skip to content

OOP in C++ — Level 3

Polymorphism, Overriding, Virtual Functions & Abstract Classes


1. Function Overloading

1.1 Definition

Function overloading means having multiple functions with the same name but different parameter lists.

class Calculator {
public:
    void add(int a, int b) {
        cout << a + b;
    }

    void add(double a, double b) {
        cout << a + b;
    }

    void add(int a, int b, int c) {
        cout << a + b + c;
    }
};

The compiler determines which function to call based on the arguments.

Calculator c;

c.add(2, 3);          // int version
c.add(2.5, 3.5);      // double version
c.add(1, 2, 3);       // three-argument version

This is compile-time polymorphism.


1.2 What Can Be Different?

Overloaded functions can differ in:

Number of parameters

void fun(int);
void fun(int, int);

Type of parameters

void fun(int);
void fun(double);

Order of parameter types

void fun(int, double);
void fun(double, int);

1.3 Return Type Alone Cannot Overload

This is invalid:

int fun(int x);
double fun(int x);    // ERROR

The parameter list is identical.

The compiler cannot choose between them based only on return type.

Remember

Return type is not sufficient for function overloading.


2. const and Function Overloading

Member functions can also be overloaded based on whether they are const.

class Test {
public:
    void fun() {
        cout << "Non-const";
    }

    void fun() const {
        cout << "Const";
    }
};

Now:

Test t;
const Test ct;

t.fun();      // Non-const
ct.fun();     // Const

A const object prefers the const member function.


3. Default Arguments + Overloading Trap

Consider:

void fun(int x);

void fun(int x, int y = 10);

Now:

fun(5);

This is ambiguous.

Why?

Both functions can accept one argument:

fun(int)
fun(int, int = 10)

So the compiler cannot uniquely determine which one to call.

Placement Trap

Default arguments can create ambiguity when combined with overloaded functions.


4. Implicit Conversion During Overloading

Suppose:

void fun(int);
void fun(double);

Calling:

fun('A');

'A' is a char.

Both conversions are possible, but conversion to int is a better match than conversion to double.

Therefore:

fun(int);

is selected.

Important

The compiler considers the quality of conversions while resolving overloads.


5. Operator Overloading

5.1 Definition

Operator overloading allows operators such as:

+
-
*
/
==
<
>
++
--

to work with user-defined types.

Example:

class Point {
public:
    int x, y;

    Point(int x, int y) : x(x), y(y) {}

    Point operator+(const Point& other) {
        return Point(x + other.x, y + other.y);
    }
};

Now:

Point p1(10, 20);
Point p2(5, 7);

Point p3 = p1 + p2;

The expression:

p1 + p2

is conceptually:

p1.operator+(p2);

6. Binary Operator as a Member Function

For:

p1 + p2

when operator+ is a member function:

p1.operator+(p2);

Therefore:

left operand  → current object (`this`)
right operand → function parameter

For example:

Point operator+(const Point& other)

Here:

p1 → this
p2 → other

7. Operator Overloading Is Compile-Time Polymorphism

Operator overloading is resolved during compilation.

Therefore:

Function overloading
Operator overloading
        ↓
Compile-time polymorphism

8. Unary Operator Overloading

Unary operators operate on one operand.

Examples:

++
--
!
-

Example:

class Number {
public:
    int x;

    void operator++() {
        ++x;
    }
};

9. Prefix vs Postfix ++

C++ distinguishes prefix and postfix increment using a dummy int parameter.

Prefix

void operator++() {
    ++x;
}

Postfix

void operator++(int) {
    x++;
}

The int is a dummy parameter used only to distinguish postfix from prefix.


10. Operators That Cannot Be Overloaded

Some operators cannot be overloaded.

Important examples:

::
.
.*
?:
sizeof
typeid

Also remember:

You cannot create a completely new operator.

You can only overload operators that already exist in C++.


11. Restrictions on Operator Overloading

Operator overloading cannot change:

  • operator precedence
  • operator associativity
  • number of operands

For example, you cannot make + behave as if it were a ternary operator.

Also, at least one operand must be a user-defined type when defining an overloaded operator.

You cannot redefine an operator purely for built-in types such as:

int + int

12. Function Overriding

12.1 Definition

Function overriding occurs when a derived class provides its own implementation of a virtual function inherited from the base class.

Example:

class Base {
public:
    virtual void fun() {
        cout << "Base";
    }
};

class Derived : public Base {
public:
    void fun() override {
        cout << "Derived";
    }
};

Derived::fun() overrides Base::fun().


13. Overriding Requires Compatible Signature

Suppose:

class Base {
public:
    virtual void fun(int) {}
};

This correctly overrides it:

class Derived : public Base {
public:
    void fun(int) override {}
};

But:

void fun(double) override;

does not override it.

Likewise:

void fun() override;

does not override fun(int).


14. const Is Part of the Function Signature for Overriding

Consider:

class Base {
public:
    virtual void fun() const {}
};

This is not an override:

class Derived : public Base {
public:
    void fun() override {}
};

because:

Base:    fun() const
Derived: fun()

They are different member-function signatures.

Correct:

void fun() const override {}

15. Return Type in Overriding

You cannot arbitrarily change the return type:

class Base {
public:
    virtual int fun() {
        return 10;
    }
};

class Derived : public Base {
public:
    double fun() override {   // ERROR
        return 2.5;
    }
};

The return type must be compatible.

There is an advanced exception called covariant return types, involving certain related class pointer/reference return types.

For basic placement preparation:

Do not assume you can change the return type while overriding.


16. Access Modifier and Overriding

The access modifier can differ between the base and derived versions.

class Base {
public:
    virtual void fun() {}
};

class Derived : public Base {
private:
    void fun() override {}
};

This is still an override.

Therefore:

Access control and overriding are separate concepts.

The derived function can be private, even if the base function is public.


17. Once Virtual, It Remains Virtual

If:

class Base {
public:
    virtual void fun() {}
};

then:

class Derived : public Base {
public:
    void fun() override {}
};

Derived::fun() is still virtual.

A further derived class can override it:

class Child : public Derived {
public:
    void fun() override {}
};

You don't need to write virtual again.


18. Virtual Functions

18.1 Why Virtual Functions?

Consider:

class Base {
public:
    void fun() {
        cout << "Base";
    }
};

class Derived : public Base {
public:
    void fun() {
        cout << "Derived";
    }
};

Now:

Base* ptr = new Derived();

ptr->fun();

Output:

Base

because fun() is not virtual.

The call is resolved using the static type:

Base*

18.2 Adding virtual

class Base {
public:
    virtual void fun() {
        cout << "Base";
    }
};

Now:

Base* ptr = new Derived();

ptr->fun();

Output:

Derived

This is dynamic dispatch.


19. Static Type vs Dynamic Type

Consider:

Animal* ptr = new Dog();

The pointer has:

Static type = Animal*

The actual object is:

Dynamic type = Dog

For a virtual function call, the dynamic type determines which implementation executes.

Animal* ptr
     ↓
   Dog object
     ↓
virtual function
     ↓
Dog implementation

20. vtable and vptr

The C++ standard does not require a specific implementation using vtable and vptr.

However, most mainstream C++ implementations use a mechanism conceptually similar to this.

vtable

A virtual table contains entries associated with virtual functions.

Conceptually:

Base vtable
----------------
fun → Base::fun

Derived:

Derived vtable
----------------
fun → Derived::fun

If Derived does not override another virtual function:

Derived vtable
----------------
fun → Derived::fun
other → Base::other

21. vptr

A polymorphic object typically contains a hidden pointer called a vptr.

Conceptually:

Dog object
+----------------+
| vptr ----------|----> Dog vtable
+----------------+
| data members   |
+----------------+

The vtable then points to the appropriate virtual functions.

Again, this is an implementation model, not a direct C++ language requirement.


22. Virtual Functions and Object Size

Because typical implementations use a hidden vptr, objects containing virtual functions may have additional memory overhead.

For example:

class Base {
public:
    virtual void fun() {}
    int x;
};

A typical implementation may store:

vptr
x
padding

Therefore, don't blindly calculate object size by simply adding visible data members when virtual dispatch is involved.


23. Static Member Functions Cannot Be Virtual

A static member function has no this pointer because it doesn't belong to a particular object.

Virtual dispatch requires an object.

Therefore:

static virtual void fun();  // invalid

Static functions cannot be virtual.


24. Constructors Cannot Be Virtual

Constructors cannot be virtual because the object is not fully constructed yet and virtual dispatch requires an already-existing object.

virtual Base();   // invalid

Destructors, however, can be virtual.

Virtual destructors are covered in Level 4.


25. Pure Virtual Functions

A pure virtual function is declared using:

= 0

Example:

class Animal {
public:
    virtual void sound() = 0;
};

This essentially says:

Derived concrete classes are expected to provide their own implementation.


26. Abstract Class

A class containing at least one pure virtual function is an abstract class.

class Animal {
public:
    virtual void sound() = 0;
};

You cannot directly create an object:

Animal a;    // ERROR

But you can create a pointer/reference:

Animal* ptr;
Animal& ref = dog;

And you can point a base pointer toward a concrete derived object:

Animal* ptr = new Dog();

27. Abstract Class Can Have Normal Functions

An abstract class does not have to contain only pure virtual functions.

It can contain:

  • data members
  • constructors
  • destructors
  • normal functions
  • virtual functions
  • pure virtual functions

Example:

class Animal {
protected:
    string name;

public:
    Animal(string n) : name(n) {}

    void info() {
        cout << name;
    }

    virtual void sound() = 0;
};

This is still abstract because it has a pure virtual function.


28. Abstract Class Can Have a Constructor

Although:

Animal a;

is invalid, the constructor of Animal can still run when constructing a derived object.

class Animal {
public:
    Animal() {
        cout << "Animal constructor\n";
    }

    virtual void sound() = 0;
};

class Dog : public Animal {
public:
    Dog() {
        cout << "Dog constructor\n";
    }

    void sound() override {}
};
Dog d;

Output:

Animal constructor
Dog constructor

The base constructor is needed to initialize the base portion of the derived object.


29. Pure Virtual Function Can Have a Definition

A pure virtual function can actually have a function body.

class Base {
public:
    virtual void fun() = 0;
};

void Base::fun() {
    cout << "Base implementation";
}

The class is still abstract.

A derived class can call that implementation explicitly:

class Derived : public Base {
public:
    void fun() override {
        Base::fun();
        cout << " Derived";
    }
};

30. Pure Virtual Destructor

A destructor can also be pure virtual:

class Base {
public:
    virtual ~Base() = 0;
};

But a pure virtual destructor must have a definition:

Base::~Base() {
}

The class becomes abstract, but the base destructor still needs an implementation because the base portion of the object must eventually be destroyed.

Virtual destructors themselves are covered in detail in Level 4.


31. Interface-Like Classes

C++ does not have a dedicated:

interface

keyword.

Instead, interface-like behavior is commonly achieved using an abstract class containing mostly/all pure virtual functions.

Example:

class Payment {
public:
    virtual void pay() = 0;
    virtual void refund() = 0;

    virtual ~Payment() {}
};

Different classes can implement the interface:

class UPI : public Payment {
public:
    void pay() override {}
    void refund() override {}
};
class Card : public Payment {
public:
    void pay() override {}
    void refund() override {}
};

32. Runtime Polymorphism

32.1 Definition

Runtime polymorphism means that the implementation to execute is determined at runtime based on the actual object.

The classic C++ pattern is:

Base* ptr = new Derived();
ptr->virtualFunction();

which calls:

Derived::virtualFunction()

33. Requirements for Runtime Polymorphism

The classic pattern involves:

  1. Inheritance
  2. Virtual function in the base class
  3. Overriding in derived class
  4. Base pointer/reference
  5. Base pointer/reference referring to a derived object
  6. Virtual function call

Conceptually:

Inheritance
     ↓
Base pointer/reference
     ↓
Derived object
     ↓
Virtual function
     ↓
Dynamic dispatch
     ↓
Derived implementation

34. Runtime Polymorphism Using a Pointer

class Animal {
public:
    virtual void sound() {
        cout << "Animal\n";
    }
};

class Dog : public Animal {
public:
    void sound() override {
        cout << "Dog\n";
    }
};

Animal* ptr = new Dog();

ptr->sound();

Output:

Dog

Although:

ptr

has type:

Animal*

the actual object is:

Dog

35. Runtime Polymorphism Using a Reference

It also works with references.

Dog d;

Animal& ref = d;

ref.sound();

Output:

Dog

Therefore:

Runtime polymorphism can work through both base-class pointers and base-class references.


36. Why Runtime Polymorphism Is Useful

Suppose we have:

class Payment {
public:
    virtual void pay() = 0;
};

class CreditCard : public Payment {
public:
    void pay() override {
        cout << "Credit Card\n";
    }
};

class UPI : public Payment {
public:
    void pay() override {
        cout << "UPI\n";
    }
};

We can write:

void processPayment(Payment& p) {
    p.pay();
}

Then:

CreditCard card;
UPI upi;

processPayment(card);
processPayment(upi);

Output:

Credit Card
UPI

The function doesn't need to know the exact concrete type.

This provides:

  • extensibility
  • loose coupling
  • common interfaces
  • substitutability of derived objects
  • cleaner object-oriented design

37. Collection of Different Derived Objects

Runtime polymorphism is especially useful when multiple derived types need to be handled uniformly.

Animal* animals[2];

Dog d;
Cat c;

animals[0] = &d;
animals[1] = &c;

for(int i = 0; i < 2; i++) {
    animals[i]->sound();
}

Output:

Dog
Cat

The array contains Animal*, but the objects can be different derived types.


38. Static Binding vs Dynamic Binding

Static Binding

Function call is determined at compile time.

class Base {
public:
    void fun() {
        cout << "Base";
    }
};

class Derived : public Base {
public:
    void fun() {
        cout << "Derived";
    }
};

Base* ptr = new Derived();

ptr->fun();

Output:

Base

Because fun() is not virtual.

The compiler uses the static type:

Base*

Dynamic Binding

With:

class Base {
public:
    virtual void fun() {
        cout << "Base";
    }
};

Then:

Base* ptr = new Derived();

ptr->fun();

Output:

Derived

The dynamic type of the object determines the implementation.


39. Direct Object Calls

Virtual functions do not mean every call automatically requires runtime dispatch.

Consider:

Dog d;

d.sound();

The compiler already knows that d is a Dog.

The interesting runtime-polymorphism situation is:

Animal* ptr = &d;

ptr->sound();

because:

Static type  = Animal*
Dynamic type = Dog

40. override

The override keyword tells the compiler:

"I intend this function to override a virtual function from the base class. Verify that it actually does."

Example:

class Base {
public:
    virtual void show() {}
};

class Derived : public Base {
public:
    void show() override {}
};

41. Why override Is Useful

Without override, this mistake may go unnoticed:

class Base {
public:
    virtual void show(int) {}
};

class Derived : public Base {
public:
    void show() {
    }
};

The programmer may think they overrode show, but they didn't.

With:

void show() override

the compiler catches the mismatch.


42. override Does Not Create Overriding

The keyword does not magically turn a function into an override.

It only asks the compiler to verify the intended override.

So:

virtual → enables virtual dispatch

override → verifies that overriding actually occurs

43. final

final prevents further overriding.

Example:

class Base {
public:
    virtual void fun() final {
        cout << "Base";
    }
};

Now:

class Derived : public Base {
public:
    void fun() override {}    // ERROR
};

because the function is final.


44. final on a Class

A class can also be declared final:

class Base final {
};

Now:

class Derived : public Base {
};

is illegal.

Therefore:

final function → cannot override further

final class → cannot inherit from it

45. Combining override and final

Both can be used together:

class Base {
public:
    virtual void fun() {}
};

class Derived : public Base {
public:
    void fun() override final {}
};

Meaning:

  • Derived::fun() correctly overrides Base::fun()
  • No class derived from Derived can override fun()

46. override vs final

Keyword Meaning
override Verify that this function overrides a base virtual function
final on function Prevent further overriding
final on class Prevent inheritance

Memory trick:

override → "I am overriding."
final    → "Stop here."

47. Common Placement Traps

Trap 1 — Same name doesn't mean overriding

Base:
fun(int)

Derived:
fun(double)

Not an override.


Trap 2 — const mismatch

Base:
fun() const

Derived:
fun()

Not an override.


Trap 3 — Return type mismatch

Base:
int fun()

Derived:
double fun()

Invalid overriding.


Trap 4 — Non-virtual function

Base* p = new Derived();
p->fun();

If fun() isn't virtual:

Base

If fun() is virtual:

Derived

Trap 5 — override catches mistakes

void fun(double) override

when base has:

virtual void fun(int)

→ compilation error.


Trap 6 — final prevents overriding

virtual void fun() final;

A derived class cannot override it.


48. Compile-Time vs Runtime Polymorphism

Compile-Time Polymorphism Runtime Polymorphism
Function overloading Function overriding
Operator overloading Virtual functions
Decision at compile time Decision at runtime
Static binding Dynamic binding
No runtime virtual dispatch Virtual dispatch
fun(int) vs fun(double) Base* → Derived

49. Mixed Example

Consider:

class Base {
public:
    virtual void fun() {
        cout << "Base ";
    }

    void show() {
        cout << "BaseShow ";
    }
};

class Derived : public Base {
public:
    void fun() override {
        cout << "Derived ";
    }

    void show() {
        cout << "DerivedShow ";
    }
};

Now:

Derived d;
Base* ptr = &d;

ptr->fun();
ptr->show();
d.fun();
d.show();

Output:

Derived BaseShow Derived DerivedShow

Why?

ptr->fun()

fun() is virtual:

Base* → Derived object
       ↓
dynamic dispatch
       ↓
Derived::fun()

→ Derived

ptr->show()

show() is not virtual:

Base* → Base::show()

→ BaseShow

d.fun()

Direct Derived object:

→ Derived::fun()

d.show()

Direct Derived object:

→ Derived::show()


50. Most Important Mental Model

Whenever you see:

Base* p = &derived;
p->function();

ask:

Step 1

Is function() virtual?

If NO:

Static binding
→ Base implementation

If YES:

Dynamic binding
→ Derived implementation

This simple decision solves a large number of C++ placement questions.


51. Placement Quick Revision

Function Overloading

Same name
Different parameters
Compile time

Operator Overloading

Give existing operators meaning for user-defined types
Compile time

Function Overriding

Derived provides implementation of inherited virtual function
Runtime polymorphism when called virtually through base pointer/reference

Virtual Function

Enables dynamic dispatch

Pure Virtual Function

virtual void fun() = 0;

Creates a contract and makes the class abstract.

Abstract Class

Cannot instantiate directly
Can have constructors, destructors, data members and normal functions
Can be used through pointers/references

Runtime Polymorphism

Base pointer/reference
+
Derived object
+
Virtual function
=
Dynamic dispatch

override

Compiler verifies intended overriding

final

Prevents further overriding/inheritance

Level 3 — Final Concept Map

                         POLYMORPHISM
                              |
              +---------------+---------------+
              |                               |
       Compile-Time                      Runtime
              |                               |
       +------+-------+                       |
       |              |                       |
 Function         Operator              Virtual Function
 Overloading      Overloading                 |
                                              |
                                         Overriding
                                              |
                                    Base Pointer/Reference
                                              |
                                        Derived Object
                                              |
                                       Dynamic Dispatch
                                              |
                                    Derived Implementation


               VIRTUAL / ABSTRACT CONCEPTS
                          |
             +------------+------------+
             |                         |
       Virtual Function         Pure Virtual Function
             |                         |
       Dynamic Dispatch         Abstract Class
                                       |
                                Interface-like Design


                  OVERRIDING CONTROL
                          |
                 +--------+--------+
                 |                 |
              override           final
                 |                 |
          Verify override     Stop overriding
                                   |
                              final class
                                   |
                          Stop inheritance

Level 3 — Interview Checklist

Before an interview, make sure you can answer these without hesitation:

  • What is function overloading?
  • Why can't return type alone overload a function?
  • How can const member functions be overloaded?
  • How can default arguments cause ambiguity?
  • What is operator overloading?
  • What does p1 + p2 become conceptually for a member operator+?
  • What operators cannot be overloaded?
  • What is function overriding?
  • What makes overriding different from hiding?
  • Why is const important in overriding?
  • What does override do?
  • What does final do?
  • What is a virtual function?
  • What is dynamic dispatch?
  • What is static binding vs dynamic binding?
  • What are static and dynamic types?
  • What are vtable and vptr conceptually?
  • Why can't static member functions be virtual?
  • Why can't constructors be virtual?
  • What is a pure virtual function?
  • What is an abstract class?
  • Can an abstract class have a constructor?
  • Can a pure virtual function have a definition?
  • Can a pure virtual destructor have a definition?
  • Does C++ have an interface keyword?
  • What is runtime polymorphism?
  • Why are base pointers/references important?
  • What happens when a base function is not virtual?
  • What happens when a derived function is called directly?
  • Why is runtime polymorphism useful in real systems?
  • What is the classic Base* → Derived pattern?

One-Line Level 3 Summary

C++ polymorphism allows the same interface to represent different behaviors: overloading and operator overloading provide compile-time polymorphism, while virtual functions, overriding, and base pointers/references provide runtime polymorphism.