8 min read
CS Fundamentals Index
Tier 1 -- Foundations
Object-Oriented Programming
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
CS Fundamentals Index
Tier 1 -- Foundations
Object-Oriented Programming
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
Inheritance & Polymorphism
1. What Is It?
lets one reuse and extend another. You declare Dog extends Animal (Java) or class Dog(Animal) (Python), and Dog automatically gets Animal's fields and methods — plus whatever Dog adds or overrides. The goal is reuse without copy-paste, and a shared vocabulary for related types.
is the payoff: code written against the parent type works unchanged when you pass a child type. A function that takes Animal can receive any Dog, Cat, or Sparrow, and calling animal.speak() runs the right version for each. Without , every new type forces you to open up and edit every function that handles it — the classic cascade of if isinstance(...) or switch(type) blocks.
A backend service handles payments and currently has a processPayment(CreditCardPayment payment) function. The team needs to add support for PayPal and crypto payments. Without polymorphism, what is the most likely maintenance problem they will face?
2. How It Works
Inheritance: the "is-a" link
When Dog inherits from Animal, a Dog has everything an Animal has. The parent's fields and methods are present in the child, and the child can:
- Add new fields and methods
- Override a parent method to change its behavior
- Call the parent's version via
super()/super.method()/Parent::method()
# Python class Animal: def __init__(self, name): self.name = name def speak(self): return "..." class Dog(Animal): def speak(self): # override return f"{self.name} says woof" class Puppy(Dog): def speak(self): return super().speak() + " (small)" # reuse parent print(Puppy("Rex").speak()) # Rex says woof (small)
// Java public class Animal { protected String name; public Animal(String name) { this.name = name; } public String speak() { return "..."; } } public class Dog extends Animal { public Dog(String name) { super(name); } @Override public String speak() { return name + " says woof"; } }
// C++ class Animal { public: Animal(std::string name) : name_(std::move(name)) {} virtual std::string speak() const { return "..."; } // 'virtual' = overridable virtual ~Animal() = default; // virtual destructor! protected: std::string name_; }; class Dog : public Animal { public: Dog(std::string name) : Animal(std::move(name)) {} std::string speak() const override { return name_ + " says woof"; } };
Polymorphism: one call, many behaviors
# Python — duck typing; no declaration needed def describe(animal): print(animal.speak()) describe(Dog("Rex")) # Rex says woof describe(Puppy("Max")) # Max says woof (small)
// Java — variable type is Animal, runtime picks the right method Animal a = new Dog("Rex"); System.out.println(a.speak()); // "Rex says woof" — dynamic dispatch
// C++ — dynamic dispatch requires a pointer/reference AND 'virtual' Animal* a = new Dog("Rex"); std::cout << a->speak(); // "Rex says woof" delete a; Animal b = Dog("Rex"); // OBJECT SLICING — b is now just an Animal std::cout << b.speak(); // "..." — Dog part was sliced off
The vtable — how dynamic dispatch works
When a method is polymorphic, the compiler can't decide at compile time which function to call — it depends on the 's actual type at runtime. Languages implement this with a vtable (virtual method table): each has a table of function pointers, and each object carries a hidden pointer to its 's table. A call like a.speak() becomes "look up speak in a's vtable, call that."
- Python / Java: every method is dispatched through this mechanism by default.
- C++: only methods marked
virtualare. Non-virtual calls are resolved at compile time and are faster — but won't do .
A backend service stores a collection of shape objects in C++. A developer writes the following code to compute the total area:
Despitestd::vector<Shape> shapes; shapes.push_back(Circle(5.0)); shapes.push_back(Rectangle(3.0, 4.0)); for (Shape s : shapes) { std::cout << s.area() << "\n"; }
Circle and Rectangle overriding area(), every call prints the base Shape::area() result. What is the most likely cause?3. What You Actually Need to Know
- Overriding vs. overloading are different. Overriding replaces a parent's method in a child (runtime dispatch). Overloading means multiple methods with the same name but different parameters in the same (compile-time; Python effectively doesn't have it).
- Use
@Override(Java) oroverride(C++11+). These make the compiler check you actually overrode something — catches typos likeequals()vs.equal()that silently create a new method. - C++: mark destructors
virtualin any class you might inherit from. Otherwisedelete parent_ptrwon't run the child's destructor — resource leak. - C++: beware object slicing. Assigning a
Dogto anAnimal(by value) copies only theAnimalpart. Use pointers or references for . - Calling parent methods —
super().method()(Python),super.method()(Java),Parent::method()(C++). Common in__init__/ constructors so the parent initializes its own fields. - Debugging clues:
- "The parent's method ran when I expected the child's" — in C++, the method isn't
virtual, or you're calling by value (slicing). In Java, you declared itstaticorfinal. In Python, check your class hierarchy / MRO. NoSuchMethodErrorafter refactoring (Java) — a caller was compiled against an older class version; the method signature changed.- Infinite in
__init__(Python) — you typedself.__init__(...)instead ofsuper().__init__(...).
- "The parent's method ran when I expected the child's" — in C++, the method isn't
Abstract classes and interfaces
Sometimes the parent is never meant to be instantiated — it just defines a contract. That's an , and a method with no body is an abstract method.
# Python from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): ... # Shape() → TypeError: Can't instantiate abstract class
// Java public abstract class Shape { public abstract double area(); // subclasses MUST implement } // Or just declare a contract with no state: public interface Drawable { void draw(); }
// C++ — pure virtual function makes the class abstract class Shape { public: virtual double area() const = 0; virtual ~Shape() = default; };
A C++ backend service manages database connections through a ConnectionPool base class, with a PostgresPool subclass that allocates additional resources in its constructor. The service stores pool objects via ConnectionPool* pointers and calls delete on them at shutdown. A developer notices that PostgresPool's destructor never runs, causing resource leaks. What is the most likely cause?
4. Language Differences
| Aspect | Python | Java | C++ |
|---|---|---|---|
| Inherit syntax | class B(A): | class B extends A | class B : public A |
| Multiple inheritance | Yes — method lookup order is deterministic (C3 linearization algorithm) | No (only multiple interfaces) | Yes (diamond problem possible) |
| Call parent method | super().foo() | super.foo() | A::foo() |
| Default dispatch | Always dynamic | Always dynamic (unless final/static/private) | Static unless virtual |
| Override keyword | None (duck typed) | @Override (optional but recommended) | override (C++11+) |
| Prevent inheritance | Convention only | final class | final (C++11+) |
| Abstract class | ABC + @abstractmethod | abstract class | class with = 0 method |
| Interface | Protocols (3.8+) / duck typing | interface | Abstract class with only pure virtuals |
Python's MRO (Method Resolution Order) is worth a glance: D.__mro__ shows exactly which will be checked for a method, in order. For simple hierarchies it's obvious; for diamond it's the rule. Python computes MRO using an algorithm called C3 linearization — you rarely need the details, just know that __mro__ is the source of truth when you're unsure which method will win.
C++'s diamond problem — D inherits from both B and C, both of which inherit from A — is solved with virtual ( B : virtual public A), which ensures one shared A sub- rather than two.
A Python backend service has a class hierarchy where APIHandler inherits from both AuthMixin and LoggingMixin, and both mixins themselves inherit from a shared BaseHandler class. A developer wants to know which version of BaseHandler.setup() will be called when APIHandler invokes it. What is the most reliable way to determine this in Python?
5. Tradeoffs & Decisions
- vs. . couples child to parent forever — changes to the parent ripple into every descendant. (holding another as a field and delegating to it) is looser and easier to change. Default to composition; reach for inheritance when the relationship is genuinely "is-a" and you need .
- Deep hierarchies hurt. More than two or three levels of inheritance tends to be a warning sign. Each level adds indirection and hidden behavior. Flatten with composition.
- vs. (Java / C++). An declares a contract with no state. An can declare a contract and provide shared implementation. Choose interface when you just need the shape; abstract class when subclasses share real code.
virtualin C++ is a commitment. Addingvirtuallater to a you already released changes the 's memory layout (the vtable pointer), which forces every user of the to recompile — an "ABI break" (Application Binary Interface — the layout and calling conventions that compiled code depends on). Decide up front: is this class meant to be a polymorphic base?- Template Method vs. Strategy. Both enable "variation in one step of a fixed algorithm." Template Method uses inheritance (override the step). Strategy uses composition (inject an object with that step). Strategy is usually more flexible.
If you find yourself about to override a concrete parent method to subtly change its behavior, pause: you're probably making the parent's invariants harder to reason about. Consider holding the object instead (composition) or promoting the varying step to a strategy.
Your team maintains a published C++ library used by dozens of downstream services. You now want to allow subclasses to override a key method in one of the library's core classes, but that method is currently non-virtual. What is the primary risk of adding virtual to that method in a new release?
6. Interview Cheat Sheet
- lets a reuse and extend another; lets code written against the parent type work with any child type.
- Dynamic dispatch — the actual method called is chosen at runtime based on the 's real , via a vtable.
- Python and Java dispatch all instance methods dynamically by default. C++ requires
virtual; non-virtual calls are resolved at compile time. - Override vs. overload — override = same signature, different class (runtime); overload = same name, different parameters, same class (compile-time).
- Prefer over unless you have a genuine "is-a" relationship and need .
Follow-ups:
- "What's a vtable?" — A per-class table of function pointers. Each polymorphic holds a hidden pointer to its class's vtable; calling a virtual method is an indirect call through that table.
- "Why does C++ have
virtualbut Java doesn't?" — Java assumes dynamic dispatch everywhere (pay the cost by default); C++ lets you opt in, so non-virtual calls are as fast as regular function calls. - "What's object slicing?" — C++-specific: assigning a derived object to a base-type by value copies only the base part. Use pointers or references for polymorphism.
- "When would you use over inheritance?" — Almost always. Pick inheritance only when you truly have an "is-a" relationship and need the substitutability for polymorphism.
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.