OOPs — Object-Oriented Programming
A zero-to-hero reference for Object-Oriented Programming interviews — the four pillars, class mechanics, inheritance and polymorphism internals, SOLID, the classic design patterns, and the C++-specific gotchas that separate a textbook answer from one that shows you've actually shipped code. Examples are primarily C++, with Java called out wherever the behavior meaningfully diverges.
1. Core Principles
Encapsulation Basic
Encapsulation is the bundling of data (state) and the methods that operate on that data into a single unit (a class), combined with restricting direct access to some of an object's components. It is achieved in C++ via access modifiers (private, protected, public) and getter/setter methods that control how internal state is read or mutated.
class BankAccount {
private:
double balance; // hidden internal state
public:
BankAccount(double initial) : balance(initial) {}
void deposit(double amt) {
if (amt > 0) balance += amt; // validation lives with the data
}
bool withdraw(double amt) {
if (amt > 0 && amt <= balance) {
balance -= amt;
return true;
}
return false;
}
double getBalance() const { return balance; } // controlled read access
};
The caller can never set balance to a negative number directly — every mutation is forced through validated methods. This is the essence of encapsulation: protecting invariants.
Abstraction Basic
Abstraction means exposing only the essential features of an object while hiding the implementation details. It answers "what does this do?" rather than "how does it do it?". In C++, abstraction is achieved through abstract classes (classes with pure virtual functions) and interfaces.
class Shape { // abstraction: caller only knows "shapes have an area"
public:
virtual double area() const = 0; // implementation hidden from caller
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159265 * radius * radius; }
};
Abstraction vs Encapsulation — The Classic Confusion Intermediate
This is one of the most commonly botched interview answers because both "hide" something — but they hide different things for different reasons:
| Aspect | Encapsulation | Abstraction |
|---|---|---|
| What is hidden | Data (internal state) | Implementation complexity / logic |
| Goal | Protect data integrity, control access | Reduce complexity, show only relevant details |
| Level | Implementation-level technique | Design-level concept |
| Achieved via (C++) | Access specifiers (private/protected), getters/setters | Abstract classes, interfaces, pure virtual functions |
| Analogy | A capsule/pill — the medicine (data) is sealed inside | A car's steering wheel — you use it without knowing the steering mechanism |
Distinguishing example: A Car class exposing only start(), accelerate(), brake() is abstraction — the driver doesn't need to know about fuel injection timing or the combustion cycle. The fact that the fuel level variable is private and can only be changed via refuel() (which validates it doesn't exceed tank capacity) is encapsulation. You can have encapsulation without abstraction (a class with private fields but no meaningful simplified interface — every field just has a trivial getter/setter, so nothing is really "hidden" conceptually) and, in weakly-typed hand-wavy designs, "abstraction" without strict encapsulation (an interface described in a design doc that isn't backed by real access control). In practice, well-designed C++ classes use encapsulation as the mechanism to achieve abstraction.
If asked "aren't they the same thing?", say: "Encapsulation is about data hiding and bundling; abstraction is about hiding implementation complexity behind a simplified interface. Encapsulation is a technique; abstraction is a design goal. Encapsulation is often the mechanism used to achieve abstraction."
Inheritance Basic
Inheritance lets a class (derived/child) acquire properties and behaviors of another class (base/parent), enabling code reuse and establishing an "is-a" relationship.
class Animal {
public:
void eat() { /* shared behavior */ }
virtual void makeSound() const { /* default */ }
};
class Dog : public Animal { // Dog "is-a" Animal
public:
void makeSound() const override { /* bark */ }
};
Polymorphism Basic
Polymorphism ("many forms") allows objects of different types to be treated through a common interface, with behavior that varies by the actual type. Two flavors:
- Compile-time (static) polymorphism — resolved at compile time: function overloading, operator overloading, templates.
- Runtime (dynamic) polymorphism — resolved at runtime via virtual functions and dynamic dispatch (vtable lookup).
// Compile-time: overload resolved by the compiler based on argument types
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
// Runtime: which makeSound() runs is decided at runtime via the vtable
void announce(const Animal& a) { a.makeSound(); }
Dog d;
announce(d); // calls Dog::makeSound, not Animal::makeSound
Q: Can you have polymorphism without inheritance?
Yes — compile-time polymorphism via function/operator overloading and templates doesn't require inheritance at all. Runtime polymorphism in C++ generally does require an inheritance relationship (base class pointer/reference to a derived object) because dynamic dispatch relies on the vtable set up by the class hierarchy. In duck-typed languages (Python) or via templates/concepts (C++20), you can get polymorphic behavior without a formal inheritance hierarchy.
Q: What are the four pillars of OOP?
Encapsulation, Abstraction, Inheritance, and Polymorphism (often remembered as EAIP or AEIP). Some textbooks add "Modularity" or "Message passing" as supporting ideas, but the canonical four are these.
Q: Is C a procedural language — can you do OOP in C?
You can simulate OOP in C using structs and function pointers (e.g. a struct holding data plus a table of function pointers mimics a vtable), but C has no native support for classes, access control, or inheritance. This is exactly how C++ compilers implement virtual dispatch under the hood — a struct with a hidden function-pointer table.
2. Classes & Objects
Constructors & Destructors Basic
A constructor initializes an object when it's created; a destructor cleans up when it's destroyed. C++ constructors come in three common flavors:
class Point {
int x, y;
public:
Point() : x(0), y(0) {} // default constructor
Point(int x_, int y_) : x(x_), y(y_) {} // parameterized constructor
Point(const Point& other) : x(other.x), y(other.y) {} // copy constructor
~Point() { /* release resources, if any */ } // destructor
};
Point p1; // default
Point p2(3, 4); // parameterized
Point p3(p2); // copy constructor invoked
Point p4 = p2; // also copy construction (not assignment!)
Point p4 = p2; looks like assignment but is actually copy construction because p4 is being created, not reassigned. operator= only runs when the left-hand object already exists.
The this Pointer Basic
this is an implicit pointer available inside every non-static member function, pointing to the object the method was called on. It's used to disambiguate member variables from parameters with the same name, to return *this for method chaining, and to pass the current object to another function.
class Builder {
int value = 0;
public:
Builder& setValue(int value) {
this->value = value; // disambiguate member vs parameter
return *this; // enables chaining: b.setValue(1).setValue(2)
}
};
Static Members & Methods Basic
static members belong to the class itself, not to any instance — there is exactly one copy shared across all objects. Static methods can only access static data (no implicit this).
class Counter {
static int count; // declaration
public:
Counter() { ++count; }
static int getCount() { return count; } // no 'this' available here
};
int Counter::count = 0; // definition/initialization outside the class
Counter a, b, c;
std::cout << Counter::getCount(); // 3 — shared across all instances
Access Modifiers & Visibility in Inheritance Intermediate
public members are accessible from anywhere. private members are accessible only within the class itself (and its friends). protected members are accessible within the class and its derived classes, but not from outside.
The trickier part interviewers probe is how the inheritance access specifier (public, protected, or private inheritance) further modifies visibility in the derived class:
| Base member | public inheritance | protected inheritance | private inheritance |
|---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | not accessible | not accessible | not accessible |
Private base-class members are never directly accessible in the derived class regardless of the inheritance mode — they can only be accessed indirectly through public/protected base methods.
class defaults to private inheritance; struct defaults to public inheritance. This is the same "default access" rule that makes class members default private and struct members default public.
Constant Member Functions Intermediate
A member function marked const promises not to modify the object's state (except mutable members). It can be called on both const and non-const objects, whereas non-const methods can only be called on non-const objects.
class Vector2D {
double x, y;
mutable int accessCount = 0; // mutable: can change even in a const method
public:
Vector2D(double x_, double y_) : x(x_), y(y_) {}
double getX() const { // promises not to modify *this
++accessCount; // OK: accessCount is mutable
return x;
}
void setX(double v) { x = v; } // non-const: cannot be called on a const object
};
void printX(const Vector2D& v) {
std::cout << v.getX(); // OK — getX is const
// v.setX(5); // ERROR — setX is not const
}
Initializer Lists vs Assignment in Constructor Body Intermediate
Members initialized in the constructor's initializer list are constructed directly with the given value. Members set in the constructor body are first default-constructed, then reassigned — an extra step. For members that are references, const, or lack a default constructor, the initializer list is mandatory, not just a style preference.
class Wrapper {
const int id; // const member — must use init list
std::string& ref; // reference member — must use init list
std::vector<int> data; // has default ctor, but init list avoids double work
public:
// Correct: initializer list
Wrapper(int id_, std::string& r, std::vector<int> d)
: id(id_), ref(r), data(std::move(d)) {}
// WON'T COMPILE if attempted in the body:
// Wrapper(int id_, std::string& r) { id = id_; ref = r; } // error: const/reference
};
Also note: members are initialized in the order they are declared in the class, not the order they appear in the initializer list — a frequent source of subtle bugs when one member's initialization depends on another.
Q: Why must const and reference members be initialized in the initializer list?
Because both const variables and references must be bound to a value at the moment they are created — they cannot be default-constructed and then assigned later, since const forbids reassignment and references can't be "re-seated" to point elsewhere. The initializer list runs before the constructor body executes, at the exact point of construction.
Q: What happens if you call a virtual function from within a constructor?
It does not dispatch to the derived class's override. While a base class constructor is running, the object is still "just" a base object — the derived part hasn't been constructed yet, so the vtable pointer still points to the base class's vtable. This is a classic gotcha: calling virtual functions in constructors/destructors always resolves statically to the current class's version, not the most-derived override.
Q: Can a constructor be private? Why would you do that?
Yes. This is exactly how the Singleton pattern prevents external code from instantiating the class directly — construction is only allowed through a controlled static factory method like getInstance(). It's also used in the "named constructor idiom" and to force construction only via factory functions.
Q: What's the difference between a shallow copy and a deep copy?
A shallow copy (the compiler-generated default copy constructor) copies member values as-is — if a member is a raw pointer, both objects end up pointing to the same heap memory, leading to double-free or dangling-pointer bugs when one object is destroyed. A deep copy explicitly allocates new memory and copies the pointed-to data, so each object owns independent memory. Classes managing raw resources need a custom copy constructor (or should use smart pointers to avoid the problem entirely — see Rule of Three/Five/Zero).
3. Inheritance
Types of Inheritance Basic
- Single inheritance — one derived class inherits from exactly one base class. (Diagram in words: A → B, a single arrow.)
- Multiple inheritance — one derived class inherits from more than one base class directly. (B and C both feed into D: B → D ← C.)
- Multilevel inheritance — a chain: A is base of B, B is base of C. (A → B → C.)
- Hierarchical inheritance — one base class, multiple derived classes. (A → B, A → C, A → D — a single parent fanning out.)
- Hybrid inheritance — a combination of two or more of the above, e.g. hierarchical + multiple, which is exactly the shape that produces the diamond problem.
// Multilevel
class Animal { public: void breathe() {} };
class Mammal : public Animal { public: void feedMilk() {} };
class Dog : public Mammal { public: void bark() {} };
// Hierarchical
class Shape { public: virtual double area() const = 0; };
class Circle : public Shape { /* ... */ };
class Square : public Shape { /* ... */ };
// Multiple
class Flyer { public: void fly() {} };
class Swimmer { public: void swim() {} };
class Duck : public Flyer, public Swimmer {}; // Duck can fly() and swim()
Java and C# deliberately disallow multiple inheritance of classes (to avoid the diamond problem entirely) but allow a class to implement multiple interfaces, since interfaces (traditionally) carry no state and default-method conflicts must be resolved explicitly by the implementing class.
The Diamond Problem Intermediate
The diamond problem arises in hybrid/multiple inheritance: if B and C both inherit from A, and D inherits from both B and C, then D ends up with two separate copies of A's members — one via B, one via C. This causes ambiguity: d.someAMember doesn't know which copy to use.
class A { public: int value = 10; };
class B : public A {};
class C : public A {};
class D : public B, public C {}; // D has TWO 'value' members!
D d;
// d.value = 5; // ERROR: ambiguous — B::A::value or C::A::value?
d.B::value = 5; // must disambiguate explicitly
d.C::value = 7;
Virtual Inheritance — Solving the Diamond Advanced
Declaring the intermediate base classes as virtual bases tells the compiler that D should share a single, common subobject of A, instead of one per inheritance path.
class A { public: int value = 10; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {}; // only ONE shared 'value' now
D d;
d.value = 5; // OK — unambiguous, single A subobject
Mechanically, with virtual inheritance the compiler stores a pointer/offset (a virtual base pointer, part of the object layout, conceptually similar to a vptr) so that B and C subobjects can both locate the single shared A subobject at runtime, rather than each embedding their own copy. This adds a small memory/indirection overhead compared to non-virtual inheritance — one of the reasons virtual inheritance is used sparingly and only when the diamond genuinely needs to be collapsed.
With virtual inheritance, the most-derived class (D) is responsible for initializing the virtual base (A) directly — even though A isn't D's immediate parent. If D doesn't explicitly initialize A in its initializer list, A's default constructor is used, silently bypassing any initialization B or C might have intended to pass through.
Constructor/Destructor Call Order in Inheritance Chains Basic
Construction proceeds base-to-derived (base class constructors run first, so the object is "built up" from the ground floor). Destruction proceeds in exactly the reverse order, derived-to-base, so a derived object is fully intact while its own destructor logic runs, and only afterward do its base parts get torn down.
class A { public: A() { std::cout << "A ctor\n"; } ~A() { std::cout << "A dtor\n"; } };
class B : public A { public: B() { std::cout << "B ctor\n"; } ~B() { std::cout << "B dtor\n"; } };
class C : public B { public: C() { std::cout << "C ctor\n"; } ~C() { std::cout << "C dtor\n"; } };
C obj;
// Output:
// A ctor
// B ctor
// C ctor
// (obj goes out of scope)
// C dtor
// B dtor
// A dtor
For multiple inheritance, base classes are constructed in the order they're listed in the class declaration (not the order in the initializer list), and destroyed in the exact reverse order. Member objects are constructed in declaration order too, after all base classes but before the derived class's own constructor body runs.
Q: In a multiple-inheritance diamond without virtual, how many times does A's constructor run for a D object?
Twice — once for the A subobject inside B, and once for the A subobject inside C, because there are genuinely two separate A subobjects. With virtual inheritance, it runs exactly once, and critically, it's D (the most-derived class) that is responsible for invoking it, not B or C.
Q: Why is calling a pure virtual function from a base class constructor undefined/dangerous?
During base construction the derived part of the object doesn't exist yet, and the vtable pointer refers to the base class's vtable. If the base constructor calls a pure virtual function directly (not through an override), there is no implementation to dispatch to at that stage — most compilers make this either call the base's own definition (if provided) or result in undefined behavior / abort if the function is genuinely pure with no body.
Q: What's the difference between multilevel and hierarchical inheritance?
Multilevel is a vertical chain (A → B → C, each new class derives from the immediately preceding one). Hierarchical is a horizontal fan-out from a single parent (A → B, A → C, A → D — siblings that share one common ancestor but don't derive from each other).
4. Polymorphism Deep Dive
Overloading vs Overriding Basic
| Aspect | Overloading | Overriding |
|---|---|---|
| Resolution time | Compile-time (static binding) | Runtime (dynamic binding, via virtual functions) |
| Relationship | Same class (or same scope) | Base and derived class |
| Signature | Must differ (params/types/count) | Must be identical (same signature, or covariant return type) |
| Purpose | Same operation, different input types | Specialize/replace base behavior in a subclass |
| Requires inheritance? | No | Yes |
| Keyword involved (C++) | None special | virtual in base, optionally override in derived |
// Overloading
class Printer {
public:
void print(int i) { /* ... */ }
void print(double d) { /* ... */ }
void print(const std::string& s) { /* ... */ }
};
// Overriding
class Base {
public:
virtual void speak() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void speak() override { std::cout << "Derived\n"; }
};
Operator Overloading Intermediate
C++ allows most operators to be redefined for user-defined types, letting objects behave like built-in types.
class Complex {
double re, im;
public:
Complex(double r = 0, double i = 0) : re(r), im(i) {}
// Member operator+ : c1 + c2
Complex operator+(const Complex& rhs) const {
return Complex(re + rhs.re, im + rhs.im);
}
// Friend function for operator<< so std::cout can appear on the left
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.re << " + " << c.im << "i";
return os;
}
};
Complex a(1, 2), b(3, 4);
Complex c = a + b; // uses operator+
std::cout << c; // uses operator<<, prints "4 + 6i"
operator<< must be a free function (usually a friend) because the left operand is std::ostream, not your class — a member operator would require the object to be on the left-hand side.
Virtual Functions, the Vtable & Vptr Mechanism Advanced
This is where most candidates can recite "virtual functions enable runtime polymorphism" but can't explain how. Here's the precise mechanism most compilers (GCC, Clang, MSVC) use:
- For every class that declares or inherits virtual functions, the compiler generates a static, class-wide array of function pointers called the vtable (virtual table) — one vtable per class, not per object.
- Each entry in the vtable points to the "most-derived" implementation of a virtual function for that class.
- Every object of such a class carries a hidden pointer, the vptr (vtable pointer), typically stored as the first few bytes of the object, that points to its class's vtable.
- When a derived class overrides a virtual function, its vtable slot for that function is overwritten to point to the derived implementation; all non-overridden slots still point to the base implementation.
- The vptr is set by the constructor — this is exactly why virtual calls inside a constructor resolve to the currently-constructing class, not a more-derived override (the vptr hasn't been updated to the derived class's vtable yet).
class Base {
public:
virtual void foo() { std::cout << "Base::foo\n"; }
virtual void bar() { std::cout << "Base::bar\n"; }
};
class Derived : public Base {
public:
void foo() override { std::cout << "Derived::foo\n"; } // overridden
// bar() not overridden — Derived's vtable slot for bar still points to Base::bar
};
// Conceptually:
// Base::vtable = [ &Base::foo, &Base::bar ]
// Derived::vtable = [ &Derived::foo, &Base::bar ]
Base* p = new Derived();
p->foo(); // "Derived::foo" — looked up via p's vptr -> Derived's vtable, slot 0
Memory-wise: this adds one pointer (vptr) of overhead per object (not per method — the vtable itself is shared, only one copy exists per class), and each virtual call costs an extra pointer dereference compared to a plain non-virtual call, which is why virtual dispatch is slightly slower and prevents the function call from being inlined.
Pure Virtual Functions & Abstract Classes Intermediate
A pure virtual function is declared with = 0 and has no (mandatory) implementation in that class. A class with at least one pure virtual function becomes an abstract class — it cannot be instantiated directly, only used as a base for concrete derived classes that implement all pure virtuals.
class Shape {
public:
virtual double area() const = 0; // pure virtual — no body required
virtual ~Shape() = default;
};
// Shape s; // ERROR: cannot instantiate abstract class
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w_, double h_) : w(w_), h(h_) {}
double area() const override { return w * h; } // must implement, or Rectangle is abstract too
};
Note: a pure virtual function can still have a body (definition) in C++ — derived classes can optionally call Base::area() explicitly — but the class remains abstract regardless because the = 0 marker is what matters, not the presence/absence of a body.
Interfaces — How C++ Simulates Them Intermediate
C++ has no dedicated interface keyword (unlike Java/C#). The idiom is a class with only pure virtual functions and no data members — sometimes called a "pure abstract base class."
class IDrawable { // interface convention: "I" prefix, all pure virtual
public:
virtual void draw() const = 0;
virtual ~IDrawable() = default; // virtual destructor is essential here
};
class ISerializable {
public:
virtual std::string serialize() const = 0;
virtual ~ISerializable() = default;
};
// A class can implement multiple "interfaces" via multiple inheritance
// since these are stateless, the diamond problem is a non-issue here
class Widget : public IDrawable, public ISerializable {
public:
void draw() const override { /* ... */ }
std::string serialize() const override { return "{}"; }
};
Java contrast: Java has a first-class interface keyword; a class uses implements for interfaces and extends for (single) class inheritance, and can implement any number of interfaces because interfaces traditionally carried no state — this sidesteps the diamond problem by design.
Why Destructors Should Be Virtual in a Polymorphic Base Class Intermediate
If a base class is meant to be used polymorphically (deleted through a base pointer) but its destructor is not virtual, deleting through the base pointer only calls the base class's destructor — the derived class's destructor is skipped entirely, leaking any resources it owns.
class Base {
public:
~Base() { std::cout << "~Base\n"; } // NOT virtual — bug!
};
class Derived : public Base {
int* buffer;
public:
Derived() : buffer(new int[1000]) {}
~Derived() { delete[] buffer; std::cout << "~Derived\n"; }
};
Base* p = new Derived();
delete p;
// Output: only "~Base" prints!
// ~Derived() never runs -> buffer is LEAKED (1000 ints, never freed)
Fix: mark the base destructor virtual:
class Base {
public:
virtual ~Base() { std::cout << "~Base\n"; } // now virtual — correct
};
// Now: delete p; prints "~Derived" then "~Base" — both run, buffer is freed.
Rule of thumb: "If a class has ANY virtual function, or is intended to be a polymorphic base class, give it a virtual destructor." If a class is never used polymorphically (never deleted through a base pointer, e.g. it's not meant to be a base at all), a virtual destructor is unnecessary overhead (extra vptr, extra indirection) — but the safe default for any base class is to make it virtual.
Covariant Return Types Advanced
Normally, an overriding function must have the exact same return type as the base version. C++ makes an exception: if the base returns a pointer/reference to a base class, the override may return a pointer/reference to a more-derived class — this is called a covariant return type.
class Animal {
public:
virtual Animal* clone() const { return new Animal(*this); }
};
class Dog : public Animal {
public:
Dog* clone() const override { return new Dog(*this); } // covariant: Dog* instead of Animal*
};
This is extremely useful for a virtual clone() method (Prototype pattern) — callers using a Dog pointer get back a Dog* directly, without needing a downcast.
The override and final Keywords (C++11) Intermediate
override tells the compiler "I intend to override a base virtual function" — if the signature doesn't actually match any base virtual function (e.g. a typo, wrong const-ness, wrong parameter types), you get a compile error instead of silently creating a new, unrelated overload. final prevents further overriding (on a method) or further inheritance (on a class).
class Base {
public:
virtual void foo(int x) const {}
};
class Derived : public Base {
public:
void foo(int x) const override {} // OK, correctly overrides
// void foo(double x) const override {} // ERROR: doesn't match any base virtual -> caught at compile time!
};
class Sealed final : public Derived {}; // no class can inherit from Sealed
class Locked : public Derived {
public:
void foo(int x) const final {} // no further class can override foo
};
Q: What's the classic bug that override catches?
Writing an override with a slightly wrong signature — e.g. forgetting const, a mismatched parameter type, or a typo in the name — creates a brand-new, unrelated overload in the derived class instead of overriding the base method. Without override, this compiles silently, and calling through a base pointer keeps invoking the base's (unintended) version. With override, the compiler flags it immediately as an error.
Q: Does adding virtual to a function have a runtime cost even if it's never overridden?
Yes, marginally — the class gains a vtable and every instance gains a vptr (extra memory per object), and calls go through an indirect pointer dereference instead of a direct call, which also defeats inlining. This is why performance-critical code sometimes avoids virtual functions in favor of templates/CRTP (static polymorphism) when the concrete type is known at compile time.
Q: Can constructors be virtual? Can destructors?
Constructors cannot be virtual — the vtable/vptr mechanism relies on the object already existing with a determined type, but during construction the object's type is still being established, so there's a chicken-and-egg problem. Destructors, however, not only can but often should be virtual in polymorphic base classes, precisely so that delete through a base pointer invokes the full derived destructor chain.
Q: What is the "vtable" actually — one per class or one per object?
One per class (or more precisely, one per class that has distinct virtual overrides). All objects of the same class share the same vtable; each individual object just carries a vptr pointing to it. This is why the vptr overhead is a fixed per-object cost, while the vtable itself is not duplicated per instance.
5. Object Relationships
Association vs Aggregation vs Composition Intermediate
These describe "has-a" relationships between objects, in increasing order of ownership strength — as opposed to inheritance's "is-a" relationship.
| Relationship | Meaning | Lifetime coupling | Example |
|---|---|---|---|
| Association | A general "uses" relationship between two independent objects | Fully independent lifetimes | A Teacher and a Student — related, but neither owns the other |
| Aggregation | "Has-a" with a whole-part relationship, but the part can exist independently | Part can outlive the whole | A Department has Professors — if the department is dissolved, the professors still exist |
| Composition | "Has-a" with strong ownership — the part's lifetime is bound to the whole | Part is destroyed when the whole is destroyed | A Car has an Engine that is created and destroyed with the car |
// Composition: Engine is owned by Car; created/destroyed with it (by value / unique_ptr)
class Engine {
public:
void start() { /* ... */ }
};
class Car {
Engine engine; // Car OWNS engine — its lifetime is tied to Car's lifetime
public:
void start() { engine.start(); }
}; // when a Car is destroyed, its Engine is automatically destroyed too
// Aggregation: Car HAS a Driver, but Driver exists independently of any Car
class Driver { public: std::string name; };
class TaxiCab {
Driver* driver; // TaxiCab does NOT own the Driver's lifetime
public:
TaxiCab(Driver* d) : driver(d) {}
// deleting a TaxiCab must NOT delete driver — the driver can drive other cabs
};
The classic example: "Car has-a Engine" is composition (an engine typically isn't shared or reused across cars, and it's destroyed with the car). "Car has-a Driver" is aggregation (the same driver can exist without that specific car, and can move to a different car). In C++, composition is often modeled with a member stored by value or a unique_ptr; aggregation is often modeled with a raw pointer/reference or a shared_ptr to an externally-owned object.
Dependency Basic
The weakest relationship: one class merely uses another temporarily, typically as a method parameter, local variable, or return type — with no persistent reference stored as a member at all.
class Logger; // forward declaration is enough for a dependency
class OrderProcessor {
public:
// OrderProcessor "depends on" Logger only for the duration of this call
void process(const Logger& logger) {
// ... use logger here, but no Logger member is stored
}
};
UML Notation Basics (Described in Words) Basic
- Inheritance ("is-a") — a solid line with a hollow (unfilled) triangle arrowhead, pointing from the derived class to the base class.
- Interface implementation — a dashed line with a hollow triangle arrowhead, pointing from the implementing class to the interface.
- Composition — a solid line with a filled diamond at the "whole" end (e.g. at Car, pointing away from Engine).
- Aggregation — a solid line with a hollow (unfilled) diamond at the "whole" end.
- Association — a plain solid line, sometimes with an arrowhead to indicate navigation direction, and multiplicity labels (e.g.
1,0..*) at each end. - Dependency — a dashed line with an open (stick) arrowhead, pointing from the dependent class to the class it depends on.
Q: Give a one-line rule to distinguish aggregation from composition.
Ask: "If the whole object is destroyed, does the part survive?" If yes → aggregation. If the part is destroyed along with the whole → composition. Equivalently: composition implies exclusive ownership and matching lifetimes; aggregation implies a shared or independently-owned part.
Q: Is "is-a" (inheritance) always the right choice over "has-a" (composition)?
No — this is precisely the "composition over inheritance" debate (covered in the Advanced section). Inheritance creates tight coupling and can violate LSP if the subclass doesn't behave fully substitutably for the base; composition is generally more flexible and testable. A common heuristic: use inheritance only when there's a genuine, stable "is-a" relationship AND you need polymorphic substitution; otherwise favor composition.
6. SOLID Principles
S — Single Responsibility Principle Intermediate
A class should have only one reason to change — i.e., one responsibility. Mixing unrelated concerns (business logic + persistence + formatting) in a single class makes it fragile and hard to test.
class Report {
public:
std::string generate() { /* build report text */ return ""; }
void saveToFile(const std::string& path) { /* file I/O */ }
void printToConsole() { /* console output */ }
// This class now has THREE reasons to change:
// report format changes, storage mechanism changes, output medium changes
};
class Report {
public:
std::string generate() { /* build report text */ return ""; }
};
class ReportSaver {
public:
void save(const Report& r, const std::string& path) { /* file I/O only */ }
};
class ReportPrinter {
public:
void print(const Report& r) { /* console output only */ }
};
O — Open/Closed Principle Intermediate
Classes should be open for extension, closed for modification — you should be able to add new behavior without editing existing, tested code. Typically achieved via polymorphism/abstraction instead of conditional branching on type.
double areaOf(const std::string& type, double a, double b) {
if (type == "rectangle") return a * b;
else if (type == "triangle") return 0.5 * a * b;
// every new shape requires editing this function
return 0;
}
class Shape { public: virtual double area() const = 0; virtual ~Shape() = default; };
class Rectangle : public Shape {
double a, b;
public:
Rectangle(double a_, double b_) : a(a_), b(b_) {}
double area() const override { return a * b; }
};
class Triangle : public Shape {
double base, height;
public:
Triangle(double b_, double h_) : base(b_), height(h_) {}
double area() const override { return 0.5 * base * height; }
};
// Adding Circle later requires ZERO changes to existing classes
L — Liskov Substitution Principle Advanced
Objects of a derived class must be substitutable for objects of the base class without altering the correctness of the program. If code that works with a base class reference breaks when handed a derived instance, LSP is violated. The canonical violation is the Rectangle/Square problem.
class Rectangle {
protected:
double width, height;
public:
virtual void setWidth(double w) { width = w; }
virtual void setHeight(double h) { height = h; }
double area() const { return width * height; }
};
class Square : public Rectangle { // "a square IS-A rectangle", mathematically true...
public:
void setWidth(double w) override { width = height = w; } // must keep both sides equal
void setHeight(double h) override { width = height = h; } // ...breaks Rectangle's contract!
};
void testArea(Rectangle& r) {
r.setWidth(5);
r.setHeight(4);
assert(r.area() == 20); // FAILS for Square! setHeight also silently changed width -> area is 16
}
Even though a square is mathematically a rectangle, modeling it as a subclass violates the base class's implicit behavioral contract ("setting width doesn't affect height"). Callers relying on Rectangle's documented behavior get surprised.
// Don't force an inheritance relationship where behavior isn't substitutable.
// Use a common interface with independent implementations instead.
class Shape { public: virtual double area() const = 0; virtual ~Shape() = default; };
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void setWidth(double w) { width = w; }
void setHeight(double h) { height = h; }
double area() const override { return width * height; }
};
class Square : public Shape { // no longer pretends to be a Rectangle
double side;
public:
Square(double s) : side(s) {}
void setSide(double s) { side = s; }
double area() const override { return side * side; }
};
I — Interface Segregation Principle Intermediate
Clients shouldn't be forced to depend on methods they don't use. Prefer several small, focused interfaces over one large "fat" interface.
class IWorker {
public:
virtual void work() = 0;
virtual void eat() = 0; // not every worker needs this...
};
class RobotWorker : public IWorker {
public:
void work() override { /* ... */ }
void eat() override { /* Robots don't eat! Forced to implement a meaningless method */ }
};
class IWorkable { public: virtual void work() = 0; };
class IFeedable { public: virtual void eat() = 0; };
class HumanWorker : public IWorkable, public IFeedable {
public:
void work() override { /* ... */ }
void eat() override { /* ... */ }
};
class RobotWorker : public IWorkable { // only implements what it actually needs
public:
void work() override { /* ... */ }
};
D — Dependency Inversion Principle Intermediate
High-level modules shouldn't depend on low-level modules directly — both should depend on abstractions. This decouples policy from implementation detail and is what makes unit testing with mocks possible.
class MySQLDatabase { // concrete, low-level detail
public:
void save(const std::string& data) { /* MySQL-specific code */ }
};
class UserService { // high-level policy, tightly coupled to MySQL
MySQLDatabase db; // hardcoded concrete dependency
public:
void registerUser(const std::string& name) { db.save(name); }
// Switching to PostgreSQL, or unit-testing with a mock, requires editing this class
};
class IDatabase { // abstraction that both sides depend on
public:
virtual void save(const std::string& data) = 0;
virtual ~IDatabase() = default;
};
class MySQLDatabase : public IDatabase {
public:
void save(const std::string& data) override { /* MySQL-specific code */ }
};
class UserService {
IDatabase& db; // depends on the abstraction, not the concrete class
public:
UserService(IDatabase& database) : db(database) {} // constructor injection
void registerUser(const std::string& name) { db.save(name); }
};
// Now a MockDatabase implementing IDatabase can be injected for unit tests
Q: Give a one-sentence definition of each SOLID letter.
Single Responsibility: one class, one reason to change. Open/Closed: extend behavior without modifying existing code. Liskov Substitution: subtypes must be usable wherever their base type is expected. Interface Segregation: don't force clients to depend on methods they don't use. Dependency Inversion: depend on abstractions, not concrete implementations.
Q: How are Dependency Inversion and Dependency Injection related?
Dependency Inversion is the design principle ("depend on abstractions"). Dependency Injection is a technique for satisfying it — supplying a class's dependencies (usually as interfaces/abstractions) from the outside, typically via constructor, setter, or a DI framework/container, rather than having the class construct its own concrete dependencies internally.
Q: Does the Rectangle/Square example mean inheritance modeling is "wrong" for is-a relationships in general?
Not in general — it means inheritance must preserve behavioral substitutability, not just structural/conceptual "is-a" truth. LSP is about contracts (pre/post-conditions, invariants), not English semantics. If a subclass can honor every method's documented behavior of the base class, inheritance is fine; if enforcing a subclass invariant (like width==height) breaks a base method's contract, prefer composition or a shared interface instead.
7. Design Patterns
The 23 "Gang of Four" design patterns fall into three families: Creational (object creation mechanisms), Structural (composing classes/objects into larger structures), and Behavioral (communication and responsibility distribution between objects).
Creational Patterns Intermediate
Singleton
Ensures a class has only one instance and provides a global access point to it. Common uses: logging, configuration managers, connection pools.
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // Meyer's Singleton — thread-safe since C++11
return instance; // (static local init is guaranteed thread-safe)
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
Factory Method
Defines an interface for creating an object, but lets subclasses decide which concrete class to instantiate.
class Shape { public: virtual void draw() = 0; virtual ~Shape() = default; };
class Circle : public Shape { public: void draw() override { /* ... */ } };
class Square : public Shape { public: void draw() override { /* ... */ } };
class ShapeFactory {
public:
static std::unique_ptr<Shape> create(const std::string& type) {
if (type == "circle") return std::make_unique<Circle>();
if (type == "square") return std::make_unique<Square>();
return nullptr;
}
};
auto s = ShapeFactory::create("circle"); // caller doesn't know the concrete class
Abstract Factory
Intent: provide an interface for creating families of related objects without specifying their concrete classes. Use case: a cross-platform UI toolkit where WindowsFactory produces a matching WindowsButton/WindowsCheckbox family, and MacFactory produces a MacButton/MacCheckbox family — guaranteeing the components produced are always visually/behaviorally consistent with each other.
Builder
Intent: separate the construction of a complex object from its representation, so the same construction process can create different representations. Use case: constructing a complex HttpRequest or SQL query object step-by-step with a fluent chained API (.setHeader(...).setBody(...).build()) instead of a constructor with a dozen optional parameters.
Prototype
Intent: create new objects by copying an existing object (a "prototype") instead of instantiating from scratch, useful when object creation is expensive or the concrete class is unknown at compile time. Use case: a graphics editor cloning a complex, already-configured shape object via a virtual clone() method (pairs naturally with covariant return types, see section 4).
Structural Patterns Intermediate
Adapter
Intent: convert the interface of a class into another interface clients expect, letting incompatible interfaces work together. Use case: wrapping a third-party XmlLogger library behind your app's own ILogger interface so the rest of the codebase never depends on the third-party API directly.
Decorator
Attaches additional responsibilities to an object dynamically, without altering other instances of the same class — an alternative to subclassing for extending behavior.
class Coffee { public: virtual double cost() const = 0; virtual ~Coffee() = default; };
class PlainCoffee : public Coffee { public: double cost() const override { return 2.0; } };
class CoffeeDecorator : public Coffee { // decorator base wraps another Coffee
protected:
std::unique_ptr<Coffee> wrapped;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : wrapped(std::move(c)) {}
};
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
double cost() const override { return wrapped->cost() + 0.5; }
};
auto order = std::make_unique<MilkDecorator>(std::make_unique<PlainCoffee>());
std::cout << order->cost(); // 2.5 — behavior composed at runtime, no new subclass needed
Facade
Intent: provide a unified, simplified interface to a set of interfaces in a complex subsystem. Use case: a VideoConverterFacade::convert(file, format) method that internally coordinates a dozen codec, buffering, and file-I/O classes so client code doesn't need to understand the subsystem's internals.
Proxy
Intent: provide a surrogate/placeholder for another object to control access to it. Use case: a VirtualImageProxy that defers loading a large image file from disk until display() is actually called (lazy loading), or an access-control proxy that checks permissions before delegating to the real object.
Composite
Intent: compose objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions of objects uniformly. Use case: a filesystem where both File and Directory implement a common FileSystemNode interface with getSize() — a directory's getSize() simply sums its children's sizes, recursively, regardless of whether each child is itself a file or another directory.
Behavioral Patterns Intermediate
Observer
Defines a one-to-many dependency so that when one object (the Subject) changes state, all its dependents (Observers) are notified automatically. Foundation of event systems, MVC, and pub-sub.
class IObserver { public: virtual void update(int newValue) = 0; virtual ~IObserver() = default; };
class Subject {
std::vector<IObserver*> observers;
int state = 0;
public:
void attach(IObserver* o) { observers.push_back(o); }
void setState(int s) {
state = s;
for (auto* o : observers) o->update(state); // notify all
}
};
class ConcreteObserver : public IObserver {
public:
void update(int newValue) override { std::cout << "Notified: " << newValue << "\n"; }
};
Strategy
Defines a family of interchangeable algorithms, encapsulates each one, and lets the algorithm vary independently of the clients that use it.
class SortStrategy { public: virtual void sort(std::vector<int>& v) = 0; virtual ~SortStrategy() = default; };
class QuickSortStrategy : public SortStrategy { public: void sort(std::vector<int>& v) override { /* ... */ } };
class BubbleSortStrategy : public SortStrategy { public: void sort(std::vector<int>& v) override { /* ... */ } };
class Sorter {
std::unique_ptr<SortStrategy> strategy;
public:
Sorter(std::unique_ptr<SortStrategy> s) : strategy(std::move(s)) {}
void setStrategy(std::unique_ptr<SortStrategy> s) { strategy = std::move(s); }
void execute(std::vector<int>& v) { strategy->sort(v); } // algorithm swapped at runtime
};
Command
Intent: encapsulate a request as an object, letting you parameterize clients with different requests, queue or log requests, and support undo. Use case: a text editor's undo/redo stack, where each edit is an object with execute() and undo() methods pushed onto a history stack.
Iterator
Intent: provide a way to access elements of an aggregate object sequentially without exposing its underlying representation. Use case: C++'s STL container iterators themselves (begin()/end()) — the same range-based for loop syntax works over a vector, list, or map regardless of internal storage.
State
Intent: allow an object to alter its behavior when its internal state changes, appearing as if it changed class. Use case: a TrafficLight object whose next() behavior differs depending on whether it's currently in the RedState, YellowState, or GreenState — each state is its own class implementing a common interface, and the context delegates to the current state object.
Template Method
Intent: define the skeleton of an algorithm in a base class method, deferring some steps to subclasses without changing the algorithm's overall structure. Use case: a DataProcessor::process() base method that calls readData() → parse() → save() in a fixed sequence, where subclasses override individual steps (e.g. CsvProcessor vs JsonProcessor) but not the overall flow.
| Category | Answers the question | Patterns covered here |
|---|---|---|
| Creational | "How is this object created?" | Singleton, Factory Method, Abstract Factory, Builder, Prototype |
| Structural | "How are objects/classes composed into larger structures?" | Adapter, Decorator, Facade, Proxy, Composite |
| Behavioral | "How do objects communicate and share responsibility?" | Observer, Strategy, Command, Iterator, State, Template Method |
Q: Factory Method vs Abstract Factory — what's the actual difference?
Factory Method creates one product via a single overridable method (often via subclassing the creator). Abstract Factory creates families of related products through multiple factory methods bundled into one interface, ensuring the created objects are mutually compatible (e.g. a whole matching UI theme's worth of widgets, not just one button).
Q: Decorator vs Inheritance — why prefer Decorator for adding behavior?
Subclassing to add every combination of feature (e.g. MilkCoffee, SugarCoffee, MilkSugarCoffee...) causes combinatorial class explosion. Decorator lets you compose independent behaviors at runtime by wrapping objects, so N independent features need only N decorator classes instead of 2^N subclasses.
Q: Strategy vs State pattern — they look structurally identical. What's the real difference?
Structurally, yes, both delegate behavior to an interchangeable object implementing a common interface. The difference is intent and control: in Strategy, the client chooses and sets the algorithm, and strategies are typically unaware of each other. In State, the state objects themselves often control transitions to other states, and the context's behavior changes as a side effect of its own internal state machine, not because a client explicitly swapped an algorithm.
Q: Why does Singleton get criticized as an anti-pattern despite being useful?
It introduces global mutable state, makes unit testing harder (hard to substitute a mock, since the class controls its own instantiation), hides dependencies (a class using Singleton::getInstance() doesn't declare that dependency in its constructor, unlike dependency injection), and can create subtle ordering/lifetime issues across translation units. Many teams prefer passing a single shared instance explicitly via dependency injection instead.
Q: Which pattern would you use to add logging around an existing method without modifying its class?
Decorator (wrap the object and add logging before/after delegating) or Proxy (if the goal is more about controlling/intercepting access rather than adding orthogonal behavior). AOP-style logging frameworks in other languages achieve something similar via interception, but in plain C++/Java OOP, Decorator is the textbook answer.
8. Language-Specific Gotchas (C++)
Object Slicing Advanced
When a derived object is assigned/copied into a base class object by value (not by pointer/reference), only the base part is copied — the derived-specific members are "sliced off" and lost, and polymorphism no longer applies at all (further calls resolve statically to the base type).
class Base {
public:
virtual void print() const { std::cout << "Base\n"; }
};
class Derived : public Base {
int extra = 42;
public:
void print() const override { std::cout << "Derived, extra=" << extra << "\n"; }
};
void process(Base b) { // takes Base BY VALUE — slicing happens here
b.print(); // ALWAYS prints "Base", even if a Derived was passed in!
}
Derived d;
process(d); // d is sliced down to a Base when copied into the parameter
Fix: pass/store by pointer or reference (const Base& or Base*) so no copy occurs and the vptr — and thus the dynamic type — is preserved.
RAII — Resource Acquisition Is Initialization Intermediate
RAII ties a resource's lifetime to an object's lifetime: acquire the resource in the constructor, release it in the destructor. Because C++ guarantees destructors run when an object goes out of scope (including during stack unwinding from an exception), RAII gives automatic, exception-safe cleanup without manual try/finally-style bookkeeping.
class FileHandle {
FILE* file;
public:
FileHandle(const char* path) : file(fopen(path, "r")) {}
~FileHandle() { if (file) fclose(file); } // guaranteed to run, even on exceptions
FileHandle(const FileHandle&) = delete; // prevent double-close (see Rule of Five)
FileHandle& operator=(const FileHandle&) = delete;
};
void readConfig() {
FileHandle f("config.txt"); // resource acquired
// ... work with f, even if an exception is thrown here...
} // ~FileHandle() runs automatically -> file is always closed
std::unique_ptr, std::vector, std::lock_guard and virtually all standard containers are RAII wrappers around raw resources (heap memory, mutexes).
Rule of Three / Five / Zero Advanced
- Rule of Three (pre-C++11): if you need to explicitly define any one of: destructor, copy constructor, or copy assignment operator — you almost certainly need to define all three, because the presence of custom resource-management logic in one implies the compiler-generated versions of the others (which just do shallow member-wise copy) are wrong.
- Rule of Five (C++11+): adds the move constructor and move assignment operator to the list, since move semantics (transferring ownership instead of copying) also need to be resource-aware for classes managing raw resources.
- Rule of Zero (modern best practice): design classes so they own no raw resources directly — delegate to RAII wrappers like
std::unique_ptr,std::shared_ptr, and standard containers, so the compiler-generated special members are already correct, and you write none of the five explicitly.
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : data(new int[n]), size(n) {}
~Buffer() { delete[] data; } // 1. destructor
Buffer(const Buffer& o) : data(new int[o.size]), size(o.size) { // 2. copy ctor
std::copy(o.data, o.data + size, data);
}
Buffer& operator=(const Buffer& o) { // 3. copy assign
if (this == &o) return *this;
delete[] data;
size = o.size;
data = new int[size];
std::copy(o.data, o.data + size, data);
return *this;
}
Buffer(Buffer&& o) noexcept : data(o.data), size(o.size) { // 4. move ctor
o.data = nullptr; o.size = 0; // steal, don't copy
}
Buffer& operator=(Buffer&& o) noexcept { // 5. move assign
if (this == &o) return *this;
delete[] data;
data = o.data; size = o.size;
o.data = nullptr; o.size = 0;
return *this;
}
};
// Rule of Zero equivalent — let std::vector manage the resource entirely:
class BufferModern {
std::vector<int> data; // compiler-generated 5 special members are already correct
public:
BufferModern(size_t n) : data(n) {}
};
Smart Pointers Intermediate
| Smart pointer | Ownership model | When to use |
|---|---|---|
std::unique_ptr<T> | Exclusive ownership — cannot be copied, only moved | Default choice for owned heap resources; zero overhead vs raw pointer |
std::shared_ptr<T> | Shared ownership via atomic reference counting; last owner frees the resource | When multiple owners genuinely need to share and independently extend the resource's lifetime |
std::weak_ptr<T> | Non-owning observer of a shared_ptr-managed object — doesn't affect the reference count | Breaking reference cycles, or observing an object that may have already been destroyed |
// shared_ptr reference counting: each copy increments a shared atomic counter;
// each destruction decrements it; the managed object is deleted when the count hits 0.
std::shared_ptr<int> a = std::make_shared<int>(42);
std::cout << a.use_count(); // 1
{
std::shared_ptr<int> b = a; // copy -> count becomes 2
std::cout << a.use_count(); // 2
} // b destroyed -> count back to 1
std::cout << a.use_count(); // 1
// Breaking a reference cycle with weak_ptr:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak_ptr here prevents a cycle -> both would otherwise leak
};
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->prev = n1; // if this were shared_ptr instead of weak_ptr, n1 and n2 would keep
// each other's count >= 1 forever -> neither is ever freed (memory leak)
Two shared_ptrs that reference each other (a cycle, e.g. a parent holding a shared_ptr to a child and the child holding a shared_ptr back to the parent) will never reach a reference count of zero — neither object is ever destroyed, even when both are otherwise unreachable from the rest of the program. Always use weak_ptr for the "back-reference" side of a parent/child or observer relationship.
dynamic_cast & RTTI Advanced
dynamic_cast performs a runtime-checked downcast (base pointer/reference → derived pointer/reference), relying on RTTI (Run-Time Type Information) that the compiler embeds for polymorphic classes (classes with at least one virtual function). If the cast is invalid, it returns nullptr for pointers or throws std::bad_cast for references.
class Animal { public: virtual ~Animal() = default; }; // must be polymorphic for dynamic_cast
class Dog : public Animal { public: void bark() {} };
class Cat : public Animal {};
Animal* a = new Dog();
if (Dog* d = dynamic_cast<Dog*>(a)) {
d->bark(); // safe: cast succeeded
} else {
// cast failed -> a is not actually a Dog
}
Animal* c = new Cat();
Dog* wrong = dynamic_cast<Dog*>(c); // returns nullptr — Cat is not a Dog
dynamic_cast requires RTTI to be enabled (default in most compilers) and only works on polymorphic types. It has a real runtime cost (walking the class hierarchy) and needing it frequently is often a design smell — it suggests you should be using virtual dispatch instead of manually branching on concrete type.
Why Multiple Inheritance Is Discouraged in Most Style Guides Intermediate
- Diamond problem — ambiguous shared state unless virtual inheritance is used (and virtual inheritance has its own complexity and overhead cost).
- Increased coupling & complexity — construction/destruction order, name clashes between unrelated base classes, and layout complexity (see the vtable-for-MI section below) all make the mental model harder to reason about.
- Fragile base class problem is compounded — a change to any one of several base classes can ripple into the derived class in ways that are harder to predict than with a single base.
- Google's C++ Style Guide, for instance, allows multiple inheritance only when all but one base class are "pure interfaces" (no data members, only pure virtual functions) — precisely because interface-only multiple inheritance sidesteps the diamond problem and layout complexity entirely.
Q: Does dynamic_cast work on a non-polymorphic class?
No — it requires the class to have at least one virtual function (so RTTI/vtable-derived type info exists). Attempting dynamic_cast on a non-polymorphic type is a compile-time error, not a runtime failure.
Q: static_cast vs dynamic_cast vs reinterpret_cast — when would you use each?
static_cast is a compile-time-checked cast for related types (e.g. upcasting, numeric conversions) with no runtime safety check — fast but unsafe for downcasting unless you're certain of the type. dynamic_cast is a runtime-checked downcast for polymorphic types, safe but with runtime overhead. reinterpret_cast reinterprets raw bits with no safety checks at all (e.g. casting between unrelated pointer types) and should be reserved for low-level bit manipulation, never general-purpose downcasting.
Q: What's the practical difference between unique_ptr and shared_ptr in terms of overhead?
unique_ptr has essentially zero overhead over a raw pointer — no reference count, no control block. shared_ptr requires a control block (allocated separately unless created via make_shared, which allocates it together with the object) holding an atomic reference count, so copying a shared_ptr involves an atomic increment/decrement — meaningfully more expensive than copying a raw or unique pointer, and something to be mindful of in hot paths.
9. Advanced & Rare Topics
These come up in senior/staff-level interviews or when an interviewer wants to see if you truly understand the mechanics beneath the syntax, rather than the mechanics themselves.
CRTP — Curiously Recurring Template Pattern (Static Polymorphism) Advanced
CRTP achieves polymorphic-style behavior without virtual functions by having a base class template be parameterized on its own derived class, and calling back into the derived class's methods via a compile-time cast — the compiler resolves and can inline the "dispatch" entirely at compile time, with zero vtable/vptr overhead.
template <typename Derived>
class Shape {
public:
double area() const {
// static_cast to Derived* -- resolved entirely at compile time, no vtable lookup
return static_cast<const Derived*>(this)->areaImpl();
}
};
class Circle : public Shape<Circle> { // Circle "curiously" inherits from Shape<Circle>
double radius;
public:
Circle(double r) : radius(r) {}
double areaImpl() const { return 3.14159265 * radius * radius; }
};
Circle c(2.0);
std::cout << c.area(); // dispatches to Circle::areaImpl -- no virtual call, fully inlinable
Trade-off: CRTP requires the concrete type to be known at compile time (no runtime substitution via a common base pointer across unrelated types stored in one container), so it's a good fit for performance-critical code where the type set is fixed and known, but a poor fit when you truly need runtime-polymorphic heterogeneous collections.
Vtable Layout for Multiple Inheritance Advanced
With single inheritance, a derived object typically has one vptr, and the derived class's vtable simply extends the base's, overwriting overridden slots. With multiple inheritance, it gets considerably more complex: a class inheriting from several polymorphic base classes typically ends up with multiple vptrs — one per polymorphic base subobject — because each base subobject needs to be independently usable as a base pointer with correct dynamic dispatch.
class Base1 { public: virtual void f1() {} };
class Base2 { public: virtual void f2() {} };
class Derived : public Base1, public Base2 {
public:
void f1() override {}
void f2() override {}
};
// Conceptual memory layout of a Derived object (implementation-defined, but typical):
// [ vptr_for_Base1 subobject ] -> Derived's Base1-view vtable (f1 overridden)
// [ Base1's data members ]
// [ vptr_for_Base2 subobject ] -> Derived's Base2-view vtable (f2 overridden)
// [ Base2's data members ]
// [ Derived's own members ]
Derived d;
Base2* b2 = &d; // pointer VALUE is adjusted (this-pointer adjustment) to point at
// the Base2 subobject's start, NOT the start of the whole Derived object!
This is why, in multiple inheritance, converting a Derived* to a Base2* is not always a no-op pointer copy — the compiler may need to adjust the pointer's address ("this-pointer adjustment") to point at the correct base subobject's start. Virtual function calls through such an adjusted pointer/vtable entry may involve a "thunk" — a tiny generated stub that fixes up the this pointer before jumping to the real implementation — which is invisible in source code but very real in the compiled binary.
Step-by-Step: How a Virtual Call Resolves at Runtime Advanced
Given basePtr->virtualFunc(), here's precisely what happens at the machine level:
- Object → vptr: the compiler reads the hidden vptr field stored inside the object (typically at a fixed offset, often the very first bytes of the object for single/primary inheritance).
- vptr → vtable: the vptr's value is the address of that object's actual (most-derived) class's vtable — an array of function pointers, resolved based on the object's dynamic type, not the static pointer/reference type used to call it.
- vtable → function pointer: the compiler knows, at compile time, the fixed index/slot within the vtable that corresponds to
virtualFunc(this index is the same across the whole class hierarchy for a given virtual function signature). It reads the function pointer stored at that slot. - Function pointer → call: the CPU performs an indirect call through that function pointer, passing the (possibly this-pointer-adjusted, in the multiple-inheritance case) object pointer as the implicit first argument.
The critical insight: the slot index is fixed at compile time (the compiler always looks up "virtualFunc is slot 2", say), but which vtable is consulted is determined at runtime via the object's actual vptr. That's the entire trick behind dynamic dispatch — a compile-time-known offset combined with a runtime-known table.
Composition Over Inheritance — A Concrete Refactor Advanced
"Favor composition over inheritance" means: when you want to reuse behavior, first consider holding a reference to another object and delegating to it, rather than reflexively subclassing. Inheritance is a very strong, static, whole-class coupling; composition is looser, and swappable at runtime.
class FlyingBehaviorMixin { public: void fly() { /* generic flying */ } };
class Duck : public FlyingBehaviorMixin { /* ... */ };
class Ostrich : public FlyingBehaviorMixin { /* Ostrich inherits fly() but CAN'T fly -- broken! */ };
// forces every subclass into an "is-a flyer" relationship whether or not it's true,
// and behavior can't be swapped at runtime
class FlyBehavior { public: virtual void fly() = 0; virtual ~FlyBehavior() = default; };
class CanFly : public FlyBehavior { public: void fly() override { /* ... */ } };
class CannotFly : public FlyBehavior { public: void fly() override { /* no-op or "I can't fly" */ } };
class Bird {
std::unique_ptr<FlyBehavior> flyBehavior; // Bird HAS-A FlyBehavior, doesn't inherit it
public:
Bird(std::unique_ptr<FlyBehavior> fb) : flyBehavior(std::move(fb)) {}
void performFly() { flyBehavior->fly(); }
void setFlyBehavior(std::unique_ptr<FlyBehavior> fb) { flyBehavior = std::move(fb); } // swappable at runtime!
};
Bird duck(std::make_unique<CanFly>());
Bird ostrich(std::make_unique<CannotFly>()); // correct behavior, no LSP violation
This is essentially the Strategy pattern applied specifically to solve an inheritance-modeling problem — each behavior (flying, quacking, swimming) becomes an independently swappable, composed object instead of a rigid mixin baked into the class hierarchy.
Dependency Injection Intermediate
Dependency Injection (DI) supplies a class's collaborators from the outside rather than having the class construct them itself — the practical technique that satisfies the Dependency Inversion Principle. Constructor injection (shown below) is the most common and testable form.
class INotifier { public: virtual void send(const std::string& msg) = 0; virtual ~INotifier() = default; };
class EmailNotifier : public INotifier { public: void send(const std::string& msg) override { /* ... */ } };
class MockNotifier : public INotifier { // for unit tests -- no real email is sent
public:
std::vector<std::string> sentMessages;
void send(const std::string& msg) override { sentMessages.push_back(msg); }
};
class OrderService {
INotifier& notifier; // injected dependency, held as a reference to the abstraction
public:
OrderService(INotifier& n) : notifier(n) {} // <-- dependency injected via constructor
void placeOrder() { /* ... */ notifier.send("Order placed!"); }
};
// Production:
EmailNotifier real;
OrderService prodService(real);
// Test:
MockNotifier mock;
OrderService testService(mock);
testService.placeOrder();
assert(mock.sentMessages.size() == 1); // fully testable, no real email sent
Immutability & Value Semantics in OOP Design Advanced
An immutable object's state cannot change after construction — every "mutation" instead produces a new object. This eliminates whole categories of bugs: no aliasing surprises (two references to the same object can never observe a change made through the other), inherently thread-safe (no synchronization needed for read-only shared state), and easier to reason about.
class ImmutablePoint {
const double x, y; // const members -- cannot change after construction
public:
ImmutablePoint(double x_, double y_) : x(x_), y(y_) {}
// "Mutating" operations return a NEW object instead of modifying *this
ImmutablePoint translated(double dx, double dy) const {
return ImmutablePoint(x + dx, y + dy);
}
double getX() const { return x; }
double getY() const { return y; }
};
ImmutablePoint p1(0, 0);
ImmutablePoint p2 = p1.translated(3, 4); // p1 is untouched; p2 is a distinct new object
Value semantics means an object behaves like a primitive value (e.g. an int) — copying it produces a fully independent object, and equality compares state, not identity. This contrasts with reference semantics, where copying an object handle (a pointer/reference) still refers to the same underlying object. C++ classes default to value semantics for pass-by-value unless you deliberately use pointers/references; Java objects (aside from primitives) default to reference semantics.
Mixins / Traits as an Alternative to Inheritance Advanced
A mixin is a small, focused class designed to be "mixed in" to add a specific piece of reusable behavior, typically via multiple inheritance or (in modern C++) via templates, rather than being part of a meaningful "is-a" hierarchy. C++ commonly implements the mixin idea via CRTP-based mixins or template-based composition, since raw multiple inheritance of stateful mixins reintroduces diamond-problem risk.
template <typename Derived>
class Comparable { // mixin: adds relational operators to any class providing compareTo()
public:
bool operator<(const Derived& rhs) const {
return static_cast<const Derived*>(this)->compareTo(rhs) < 0;
}
bool operator==(const Derived& rhs) const {
return static_cast<const Derived*>(this)->compareTo(rhs) == 0;
}
};
class Money : public Comparable<Money> { // "mixes in" comparison behavior
long cents;
public:
Money(long c) : cents(c) {}
int compareTo(const Money& o) const { return (cents > o.cents) - (cents < o.cents); }
};
Money a(500), b(700);
bool cheaper = (a < b); // provided entirely by the Comparable mixin, zero virtual overhead
Java/C#'s closest equivalent is interfaces with default methods (Java 8+) or C#'s newer default interface methods — a way to bundle reusable behavior across unrelated class hierarchies without full-blown multiple class inheritance. Rust's traits are a more rigorous, first-class version of the same idea.
Q: When would you pick CRTP over a plain virtual function?
When the concrete type is known at compile time and you want to eliminate vtable/vptr overhead and enable inlining — e.g. performance-critical numeric or template-heavy libraries (Eigen, Boost use CRTP extensively). If you need to store heterogeneous objects behind a common pointer/reference at runtime (e.g. a std::vector<Shape*> of mixed concrete shapes), you need true runtime polymorphism via virtual functions instead — CRTP can't do that because each CRTP-derived class is technically a distinct, unrelated template instantiation.
Q: Explain "this-pointer adjustment" in one sentence.
When a class inherits from multiple base classes, converting a derived pointer to a non-primary base class's pointer type may require adjusting the actual address (not just the compile-time type) to point at that specific base subobject's location within the larger derived object's memory layout.
Q: Is immutability compatible with OOP, or does it fight against it?
They're compatible and increasingly combined deliberately — "functional-style OOP" using immutable value objects for data (avoiding shared mutable state bugs) while still using classes, encapsulation, and polymorphism for behavior/structure. Effective Java's advice to "minimize mutability" and C++'s const-correctness culture both push OOP code toward more immutable value objects, especially for small, frequently-copied data types.
10. Practical Coding
Implement a Thread-Safe Singleton Intermediate
The Meyer's Singleton (function-local static) is the idiomatic, thread-safe C++11+ approach — the C++11 standard guarantees that initialization of a function-local static variable is thread-safe (only one thread performs the initialization; others block until it's complete).
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // constructed exactly once, thread-safely, on first use
return instance;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "[LOG] " << msg << "\n";
}
// Explicitly delete copy/move to enforce single-instance semantics
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
private:
Logger() = default; // private constructor -- only getInstance() can create it
~Logger() = default;
std::mutex mtx;
};
// Usage:
Logger::getInstance().log("Application started");
The older, pre-C++11 approach (a heap-allocated pointer guarded by a mutex, "double-checked locking") is more error-prone and unnecessary once the compiler guarantees static-local thread safety — prefer Meyer's Singleton in any modern codebase.
Implement the Observer Pattern End-to-End Intermediate
A full Subject/Observer setup with attach, detach, and notify, plus two concrete observers reacting differently to the same event.
#include <vector>
#include <algorithm>
#include <string>
#include <iostream>
// --- Observer interface ---
class IObserver {
public:
virtual void onTemperatureChanged(double newTemp) = 0;
virtual ~IObserver() = default;
};
// --- Subject interface ---
class ISubject {
public:
virtual void attach(IObserver* o) = 0;
virtual void detach(IObserver* o) = 0;
virtual void notify() = 0;
virtual ~ISubject() = default;
};
// --- Concrete Subject ---
class WeatherStation : public ISubject {
std::vector<IObserver*> observers;
double temperature = 0.0;
public:
void attach(IObserver* o) override { observers.push_back(o); }
void detach(IObserver* o) override {
observers.erase(std::remove(observers.begin(), observers.end(), o), observers.end());
}
void notify() override {
for (auto* o : observers) o->onTemperatureChanged(temperature);
}
void setTemperature(double t) {
temperature = t;
notify(); // state change automatically triggers notification
}
};
// --- Concrete Observers ---
class PhoneDisplay : public IObserver {
public:
void onTemperatureChanged(double newTemp) override {
std::cout << "[Phone] Temp updated: " << newTemp << "C\n";
}
};
class AlertSystem : public IObserver {
public:
void onTemperatureChanged(double newTemp) override {
if (newTemp > 40.0) std::cout << "[Alert] Dangerously high temperature!\n";
}
};
// --- Usage ---
int main() {
WeatherStation station;
PhoneDisplay phone;
AlertSystem alert;
station.attach(&phone);
station.attach(&alert);
station.setTemperature(25.0); // both observers notified, only phone prints
station.setTemperature(45.0); // both observers notified, alert also fires
station.detach(&phone);
station.setTemperature(50.0); // only alert notified now
}
Implement a Strategy Pattern for a Payment System Intermediate
A checkout flow that supports multiple, independently swappable payment methods without the checkout class ever branching on payment type.
#include <memory>
#include <string>
#include <iostream>
// --- Strategy interface ---
class IPaymentStrategy {
public:
virtual bool pay(double amount) = 0;
virtual ~IPaymentStrategy() = default;
};
// --- Concrete strategies ---
class CreditCardPayment : public IPaymentStrategy {
std::string cardNumber;
public:
CreditCardPayment(std::string card) : cardNumber(std::move(card)) {}
bool pay(double amount) override {
std::cout << "Charged $" << amount << " to credit card ending in "
<< cardNumber.substr(cardNumber.size() - 4) << "\n";
return true;
}
};
class PayPalPayment : public IPaymentStrategy {
std::string email;
public:
PayPalPayment(std::string e) : email(std::move(e)) {}
bool pay(double amount) override {
std::cout << "Charged $" << amount << " via PayPal account " << email << "\n";
return true;
}
};
class CryptoPayment : public IPaymentStrategy {
std::string walletAddress;
public:
CryptoPayment(std::string w) : walletAddress(std::move(w)) {}
bool pay(double amount) override {
std::cout << "Sent $" << amount << " worth of crypto to " << walletAddress << "\n";
return true;
}
};
// --- Context ---
class ShoppingCart {
std::unique_ptr<IPaymentStrategy> paymentStrategy;
double total = 0.0;
public:
void addItem(double price) { total += price; }
void setPaymentStrategy(std::unique_ptr<IPaymentStrategy> strategy) {
paymentStrategy = std::move(strategy); // swappable at runtime, no cart code changes
}
void checkout() {
if (!paymentStrategy) {
std::cout << "No payment method selected!\n";
return;
}
paymentStrategy->pay(total);
}
};
// --- Usage ---
int main() {
ShoppingCart cart;
cart.addItem(29.99);
cart.addItem(15.50);
cart.setPaymentStrategy(std::make_unique<CreditCardPayment>("4111111111111234"));
cart.checkout(); // "Charged $45.49 to credit card ending in 1234"
cart.setPaymentStrategy(std::make_unique<PayPalPayment>("user@example.com"));
cart.checkout(); // "Charged $45.49 via PayPal account user@example.com"
}
When asked to implement any of these three patterns live, narrate the interfaces first ("I'll define an interface with one pure virtual method, then concrete implementations, then a context class that holds a pointer/reference to the interface") before writing code — interviewers are grading your design decomposition as much as your syntax.
Q: In the Strategy example, why does ShoppingCart hold a unique_ptr<IPaymentStrategy> instead of the concrete type directly?
Holding the abstraction (interface pointer) rather than a concrete type is exactly what allows the strategy to be swapped at runtime and lets new payment methods be added later without modifying ShoppingCart at all — this is Open/Closed and Dependency Inversion in action. unique_ptr is used (rather than a raw pointer) so ownership and cleanup of the strategy object are automatic and exception-safe (RAII).
Q: How would you unit test the Observer implementation above without printing to stdout?
Create a MockObserver : public IObserver that simply records the values passed to onTemperatureChanged into a member vector instead of printing, attach it to a WeatherStation, call setTemperature() a few times, and then assert on the recorded vector's contents — no I/O or printing needed, illustrating exactly why coding to an interface (rather than a concrete printing class) makes behavior independently testable.
References & Further Reading
- cppreference.com — Object-Oriented Programming in C++ (classes, inheritance, virtual functions)
- cppreference.com — Virtual Function Specifiers
- cppreference.com — Smart Pointers (unique_ptr, shared_ptr, weak_ptr)
- Refactoring.Guru — Design Patterns Catalog
- Refactoring.Guru — SOLID Principles Explained
- GeeksforGeeks — Object-Oriented Programming (OOPs) Concepts
- GeeksforGeeks — Inheritance in C++
- Gamma, Helm, Johnson, Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software (the "Gang of Four" book, Addison-Wesley, 1994) — the canonical source for the 23 classic design patterns.
- Meyers, Scott — Effective C++ and Effective Modern C++ — deep coverage of Rule of Three/Five/Zero, RAII, and smart pointer best practices.
- Standard C++ Foundation — ISO C++ FAQ
CS Prep Hub