oops-01-basics-and-pillars.md

OOP Basics and 4 Pillars — Interview Revision

Priority: MEDIUM — Likely asked as a warm-up before diving into code Spoken answer target: under 45 seconds each


1. What is OOP?

Definition:

OOP is a programming paradigm that organizes code around objects — bundles of data and behavior — rather than functions and logic. It models real-world entities as self-contained units.

Key Point / Mechanism: In procedural code, data and functions are separate — anyone can corrupt the data (e.g., set balance = -99999). OOP bundles them inside a class: data is private, behavior is exposed through controlled methods. This makes systems more maintainable, scalable, and easier to reason about.

Real-world use case: A BankAccount class hides its balance (data) and only allows deposit/withdraw (controlled methods). This is impossible in C-style code without discipline.

Likely follow-up:

"Why OOP over procedural?" OOP naturally maps to real-world problems, enforces data protection, enables code reuse via inheritance, and supports writing code that works on types not yet invented (polymorphism).

Common mistake: Thinking OOP is just about using classes. Real OOP means proper encapsulation — not exposing every field with a getter/setter.


2. Encapsulation

Definition:

Encapsulation is bundling data and the methods that operate on that data into a single unit (class), and restricting direct access to the internal state using access modifiers.

Real-world analogy (for the interview):

Think of an ATM. The internal state — cash reserves, account data, transaction logs — is completely hidden. You interact ONLY through the interface: insert card, enter PIN, select amount. You cannot reach in and grab cash directly. The ATM validates everything internally.

Key Point:

  • Data is private, methods are public
  • All changes go through methods that can validate, log, or trigger side effects
  • You can change internal implementation without breaking the caller
class BankAccount { private: double balance; public: bool withdraw(double amount) { if (amount > balance) return false; // Validates! balance -= amount; return true; } double getBalance() const { return balance; } }; // acc.balance = -99999; // Compile error — private!

Likely follow-up:

"Aren't getters/setters breaking encapsulation?" Yes, if you blindly add them for every field — that's just a struct with extra steps. Real encapsulation is adding validation, side effects, or computed properties. A class that only has getters/setters has no real encapsulation.

Common mistake: Adding getX() and setX() for every private field. If you expose everything, you have not encapsulated anything — you have just moved the problem.


3. Abstraction

Definition:

Abstraction means exposing only the essential details and hiding the underlying complexity — showing WHAT something does, not HOW it does it.

Real-world analogy (for the interview):

Driving a car. The interface you see is: steering wheel, pedals, gear shift. The complexity hidden is: engine combustion, fuel injection, electronic control units, transmission mechanics. You don't need to understand internal combustion to drive. The car abstracts away the complexity.

Key Point:

  • Implemented via abstract classes and interfaces
  • Users call sort(arr) — they don't know it uses introsort with 3-way partitioning internally
  • Lets you change implementation without breaking callers
class PaymentProcessor { public: virtual bool processPayment(double amount) = 0; // Abstract interface virtual ~PaymentProcessor() = default; }; // Caller doesn't know or care if it's Stripe, PayPal, or Razorpay void checkout(PaymentProcessor& processor, double total) { processor.processPayment(total); // Works with ANY implementation }

Likely follow-up:

"Difference between Encapsulation and Abstraction?" Encapsulation is HOW you hide (access modifiers, private data). Abstraction is WHAT you hide (complexity, unnecessary details). TV analogy: Encapsulation = the circuit board is sealed inside. Abstraction = the remote has only power/volume/channel, you never see signal decoding.

Common mistake: Treating them as the same concept. Abstraction is about simplifying the interface. Encapsulation is about protecting the internal state.


4. Inheritance

Definition:

Inheritance is a mechanism where a derived class acquires the properties and behavior of a base class, enabling code reuse and establishing an IS-A relationship.

Real-world analogy (for the interview):

A vehicle hierarchy. A Car IS-A Vehicle — it inherits color, speed, fuel capacity, and the ability to start/stop/refuel. But a Car also adds its own properties: number of doors, trunk size, ability to open the trunk. Same foundation, specialized behavior.

Key Point:

  • Use public inheritance for true IS-A relationships
  • Constructor/destructor order: Base first, Derived last (construction); Derived first, Base last (destruction)
  • Java supports single inheritance only. C++ allows multiple inheritance.
class Vehicle { public: void start() { cout << "Engine started"; } virtual ~Vehicle() = default; }; class Car : public Vehicle { // Car IS-A Vehicle int doors; public: Car(int d) : doors(d) {} void openTrunk() { cout << "Trunk opened"; } }; Car c(4); c.start(); // Inherited c.openTrunk(); // Own method

Likely follow-up:

"Inheritance vs Composition — when to use which?" Inheritance = IS-A (Dog IS-A Animal). Composition = HAS-A (Car HAS-A Engine). Prefer composition — it is more flexible, loosely coupled, and avoids fragile base class problems. Only use inheritance for true IS-A relationships that need polymorphism.

Common mistake: Using inheritance for code reuse alone. If Stack inherits from vector just to get push_back, it inherits insert, erase, and everything else — that breaks the Stack contract. Use composition.


5. Polymorphism (HIGHEST PRIORITY)

Definition:

Polymorphism means "many forms" — the same interface behaves differently depending on the underlying type. One function call, multiple behaviors.

Real-world analogy (for the interview):

A remote control's Play button. Press Play on a DVD player — it plays a movie. Press Play on a music player — it plays a song. Press Play on a game console — it resumes the game. Same button (interface), different behavior depending on the device (type). The remote does not need to know which device it is controlling.

Two types:

  1. Compile-time (static): Function overloading, operator overloading — resolved at compile time
  2. Runtime (dynamic): Virtual functions — resolved at runtime via vtable

C++ example — both types:

// COMPILE-TIME: function overloading class Printer { public: void print(int val) { cout << "Int: " << val; } void print(double val) { cout << "Double: " << val; } void print(const string& s) { cout << "String: " << s; } }; // RUNTIME: virtual functions class Shape { public: virtual double area() const = 0; virtual ~Shape() = default; }; class Circle : public Shape { double r; public: Circle(double r) : r(r) {} double area() const override { return 3.14159 * r * r; } }; class Rectangle : public Shape { double w, h; public: Rectangle(double w, double h) : w(w), h(h) {} double area() const override { return w * h; } }; void printArea(const Shape& s) { // Works for ANY shape cout << s.area(); } // printArea(Circle(5)); // 78.5 // printArea(Rectangle(4,6)); // 24

Java example (runtime polymorphism):

abstract class Shape { abstract double area(); } class Circle extends Shape { double r; Circle(double r) { this.r = r; } double area() { return Math.PI * r * r; } } class Rectangle extends Shape { double w, h; Rectangle(double w, double h) { this.w = w; this.h = h; } double area() { return w * h; } } // Works with any Shape subclass void printArea(Shape s) { System.out.println(s.area()); }

Likely follow-up:

"How does runtime polymorphism work internally?" Through vtable and vptr. Each class with virtual functions gets a vtable (array of function pointers). Each object gets a vptr pointing to its class's vtable. When you call a virtual function through a base pointer, the CPU looks up the correct function in the vtable at runtime.

Common mistake: Forgetting the virtual keyword. Without it, calling through a base pointer always calls the BASE class version — not the override. This is method hiding, not overriding.

// BUG — missing virtual class Base { void speak() { cout << "Base"; } // NOT virtual }; class Derived : public Base { void speak() { cout << "Derived"; } }; Base* p = new Derived(); p->speak(); // "Base" — WRONG! Dynamic dispatch never triggered.

Pillar Comparison Table

PillarCore IdeaMechanismReal-world
EncapsulationHide internal stateprivate + public methodsATM machine
AbstractionHide complexityAbstract classes / interfacesCar driving interface
InheritanceReuse + extendclass Derived : public BaseVehicle hierarchy
PolymorphismSame interface, many behaviorsvirtual functions / overloadingRemote control Play button