Polymorphism Deep Dive — HIGHEST PRIORITY
This is the most-tested OOP topic at MathWorks. Interviewers ask theory AND live coding on this. Spoken answer target: under 45 seconds each
1. Compile-Time Polymorphism — Function Overloading
Definition:
Multiple functions share the same name but differ in parameter type or count. The compiler decides which to call at compile time — no runtime cost.
Key Point:
- Also called static dispatch or early binding
- You CANNOT overload by return type alone
- Default arguments can cause ambiguity — watch out
class Calculator {
public:
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
// double add(int a, int b) { return ...; } // ERROR — return type differs only
};
int main() {
Calculator c;
c.add(1, 2); // Calls add(int, int)
c.add(1.5, 2.5); // Calls add(double, double)
c.add(1, 2, 3); // Calls add(int, int, int)
}Java equivalent:
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
}Likely follow-up:
"Can you overload based on return type?" No. The compiler cannot distinguish calls like
int x = add(1,2)vsdouble x = add(1,2)because the return type is inferred from context — it's ambiguous.
Common mistake: Adding a default argument that makes two overloads ambiguous:
void foo(int x, int y = 10) {}
void foo(int x) {}
foo(5); // ERROR! Compiler cannot tell which to call.2. Compile-Time Polymorphism — Operator Overloading
Definition:
Defining custom behavior for C++ operators (like +, -, ==, <<) when used with user-defined types. Resolved at compile time.
Key Point:
- Makes your class behave like a built-in type
<<and commutative binary operators must befriendfunctions (left operand is not your class)- Cannot overload:
::,.,.*,?:,sizeof,typeid - Pre-increment (++x) returns reference; Post-increment (x++) returns copy
class Vector2D {
double x, y;
public:
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Binary +: member function (left operand is *this)
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// == operator
bool operator==(const Vector2D& other) const {
return x == other.x && y == other.y;
}
// Pre-increment: returns modified object
Vector2D& operator++() {
x++; y++;
return *this;
}
// Post-increment: returns OLD value (takes dummy int param)
Vector2D operator++(int) {
Vector2D old = *this;
x++; y++;
return old;
}
// << must be friend (ostream is left operand, not Vector2D)
friend ostream& operator<<(ostream& os, const Vector2D& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
// 3.0 * vec — scalar on LEFT requires friend/non-member
friend Vector2D operator*(double s, const Vector2D& v) {
return Vector2D(s * v.x, s * v.y);
}
};
int main() {
Vector2D a(3, 4), b(1, 2);
Vector2D c = a + b; // (4, 6)
Vector2D d = 2.0 * a; // (6, 8) — uses friend operator*
cout << c << endl; // (4, 6)
++a; // a = (4, 5)
Vector2D old = a++; // old = (4, 5), a = (5, 6)
}Likely follow-up:
"Why does operator<< have to be a friend?" Because the left operand of << is
ostream, not your class. Member functions requirethis(your object) to be the left operand. Since we cannot modifyostream, we make it a free function — and grant it friend access to our private data.
Common mistake:
Overloading == but forgetting !=, or overloading < without the others. In C++20, define the spaceship operator <=> and get all comparisons for free.
3. Runtime Polymorphism — Virtual Functions
Definition:
A virtual function is a member function declared with
virtual, enabling the correct derived-class version to be called through a base-class pointer/reference at runtime.
Key Point — HOW IT WORKS (vtable/vptr):
- Compiler creates a vtable (array of function pointers) per class that has virtual functions
- Each object gets a hidden vptr pointing to its class's vtable
- When you call a virtual function through a base pointer: CPU reads vptr → looks up function in vtable → calls it
- This is dynamic dispatch — the actual type of the object determines which function runs
class Animal {
public:
virtual void speak() const {
cout << "...";
}
virtual ~Animal() {} // Virtual destructor is CRITICAL
};
class Dog : public Animal {
public:
void speak() const override {
cout << "Woof!";
}
};
class Cat : public Animal {
public:
void speak() const override {
cout << "Meow!";
}
};
int main() {
Animal* animals[] = { new Dog(), new Cat() };
for (auto* a : animals) {
a->speak(); // Calls correct version: Woof! then Meow!
// Decided at RUNTIME via vtable
}
delete animals[0];
delete animals[1];
}Java equivalent (all methods are virtual by default in Java):
abstract class Animal {
abstract void speak();
}
class Dog extends Animal {
void speak() { System.out.println("Woof!"); }
}
class Cat extends Animal {
void speak() { System.out.println("Meow!"); }
}
Animal[] animals = { new Dog(), new Cat() };
for (Animal a : animals) {
a.speak(); // Woof! then Meow! — runtime dispatch
}Likely follow-up:
"What is the vtable overhead?" Each object gets 1 extra hidden pointer (vptr, ~8 bytes). Each class has 1 vtable (shared — not per object). Each virtual call has ~2 extra memory accesses. Usually negligible unless in an extremely tight loop.
Common mistake: Calling virtual functions inside a constructor or destructor. During base class construction, the object IS a Base — the derived vtable is not yet set up. The BASE version always runs, not the derived override.
class Base {
public:
Base() { init(); } // Calls Base::init() ALWAYS, even for Derived objects!
virtual void init() { cout << "Base init"; }
};
class Derived : public Base {
void init() override { cout << "Derived init"; }
};
Derived d; // Prints "Base init", NOT "Derived init"4. Pure Virtual Functions and Abstract Classes
Definition:
A pure virtual function (
= 0) has no implementation in the base class and MUST be overridden by any concrete derived class. A class with at least one pure virtual function is abstract — it cannot be instantiated.
Key Point:
- Abstract classes define the CONTRACT — what derived classes must implement
- You CAN have pointers/references to abstract classes (enables polymorphism)
- Derived class must implement ALL pure virtual functions, or it is also abstract
class Shape {
public:
virtual double area() const = 0; // Must override
virtual double perimeter() const = 0; // Must override
virtual void draw() const = 0; // Must override
// Non-pure virtual — has default, can be overridden
virtual string color() const { return "white"; }
virtual ~Shape() = default;
};
// Shape s; // ERROR! Cannot instantiate abstract class
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() const override { return 3.14159 * r * r; }
double perimeter() const override { return 2 * 3.14159 * r; }
void draw() const override { cout << "Drawing circle r=" << r; }
};
// Polymorphism in action
vector<Shape*> shapes = { new Circle(5), new Circle(3) };
for (auto* s : shapes) {
cout << s->area() << endl; // Correct area for each shape
}Likely follow-up:
"What is an interface in C++?" C++ has no
interfacekeyword. An interface is an abstract class with ONLY pure virtual functions and no data members. Java has explicitinterfacekeyword. From Java 8, interfaces can havedefaultmethods (like C++ non-pure virtual functions).
Common mistake: Forgetting to override ONE pure virtual function in a derived class — the derived class becomes abstract too and cannot be instantiated. Compiler error: "cannot instantiate abstract class."
5. Method Overriding vs Hiding — Know the Difference
Definition:
Overriding: replacing a virtual function in a derived class — dynamic dispatch uses the override. Hiding: writing a non-virtual function in derived with same name as base — base pointer still calls BASE version.
class Base {
public:
virtual void foo(int x) { cout << "Base::foo(int)"; }
void bar() { cout << "Base::bar()"; } // NOT virtual
};
class Derived : public Base {
public:
void foo(int x) override { cout << "Derived::foo(int)"; } // OVERRIDING
void bar() { cout << "Derived::bar()"; } // HIDING
};
int main() {
Derived d;
Base* bp = &d;
bp->foo(1); // "Derived::foo(int)" — virtual dispatch (override works)
bp->bar(); // "Base::bar()" — static dispatch (hiding, base runs!)
d.bar(); // "Derived::bar()" — called directly on Derived, works
}Rule: Always use override keyword when you intend to override — it catches bugs at compile time. Missing const, wrong parameter type, or typo in name will cause a compile error instead of silent hiding.
6. Compile-Time vs Runtime — Quick Comparison
| Feature | Compile-Time | Runtime |
|---|---|---|
| Also called | Static / Early binding | Dynamic / Late binding |
| Resolved when | Compile time | Runtime |
| Mechanism | Overloading, templates | virtual functions (vtable) |
| Speed | Faster — no indirection | Small overhead (vtable lookup) |
| Keyword needed | None | virtual |
| Java default | Same concept | ALL methods are virtual by default |
| C++ example | add(int) vs add(double) | shape->area() calls right version |
7. Rapid-Fire Self-Test — Polymorphism
(Answer in one breath)
- What are the two types of polymorphism?
- What makes Round Robin scheduling an analogy for runtime polymorphism? (Hint: the scheduler doesn't know which process it runs next)
- What does the
overridekeyword catch? - Difference between hiding and overriding?
- What is a vtable and when is it created?
- Can you put different shapes in a
vector<Shape*>? Why? - If base has
void bar()(no virtual) and derived hasvoid bar(), what happens when called through base pointer? - What is the signature of a post-increment operator?
- Why must
operator<<be a friend (or non-member)? - In Java, do you need
virtualfor runtime polymorphism?
Answers
- Compile-time (function/operator overloading) and runtime (virtual functions)
- The dispatcher does not know which function to call until runtime — based on the actual object type, just like a scheduler not knowing task length
- That the function actually matches a virtual function in the base — catches typos, wrong param types, missing const
- Hiding: non-virtual, base pointer calls base version. Overriding: virtual, base pointer calls derived version via vtable
- Compiler-generated array of function pointers per class. Created at compile time, linked into the binary
- Yes — because all elements share a common base type (Shape*). The virtual dispatch handles calling the right area()
- The BASE version is called — it is static binding, not virtual dispatch
ClassName operator++(int)— takes a dummyintparameter to distinguish from pre-increment- The left operand of << is
ostream, not your class — member function requires your class as left operand - No — all non-static, non-final methods in Java are virtual by default