C++ Interview Prep · Deep Dive
The complete guide, taught from scratch. Read top to bottom — each section builds on the last.
Every function call must be resolved to an actual function. C++ has two ways:
struct Base {
void f() { std::cout << "Base::f\n"; } // non-virtual: static
virtual void g() { std::cout << "Base::g\n"; } // virtual: dynamic
};
struct Derived : Base {
void f() { std::cout << "Derived::f\n"; } // HIDES Base::f
void g() override { std::cout << "Derived::g\n"; } // OVERRIDES Base::g
};
Base* p = new Derived();
p->f(); // "Base::f" — static type of p is Base*
p->g(); // "Derived::g" — dynamic type of *p is Derived
A same-signature non-virtual function in a derived class is called redefining or hiding — not overriding. It substitutes the function only when called through the derived type.
Dynamic dispatch only happens through a pointer or reference to base. A base object by value can never behave polymorphically:
Derived d;
Base b = d; // copy-constructs a Base from d's Base subobject: SLICING
b.g(); // "Base::g" — b IS a Base, nothing to dispatch to
Base& r = d; // no copy; r refers to the actual Derived
r.g(); // "Derived::g"
Why does the sliced object dispatch to Base? The copy constructor Base(const Base&) copies only Base's members, and every constructor installs its own class's vtable pointer. The new object's identity is Base, full stop. Slicing isn't just "data got dropped" — the object's dynamic type was rewritten.
Slicing also happens when passing by value:
void byValue(Base b) { b.g(); } // always "Base::g", whatever you pass
void byRef(Base& b) { b.g(); } // dispatches properly
vtable (virtual table): a per-class static array of function pointers, one slot per virtual function in the hierarchy, built at compile time.
vptr (virtual pointer): a hidden per-object member (8 bytes on 64-bit) pointing to its class's vtable. Set by the constructor.
For p->g() where g is virtual:
*p.g's fixed slot index (known at compile time, e.g. vtable[2]).There is no runtime lookup, no "check derived, fall back to base." If Derived overrides g, its vtable slot for g holds &Derived::g; if it doesn't override, that same slot holds &Base::g. The decision was baked in when the compiler generated Derived's vtable. Dispatch is O(1) always.
Every call expr.name(args) resolves in three ordered phases. Most virtual-function traps live in the gaps between them.
expr. Find the first scope containing name, collect all declarations of name in that scope, stop. Never look further up.struct Base {
virtual void log(int) { std::cout << "int\n"; }
virtual void log(double) { std::cout << "double\n"; }
};
struct Derived : Base {
void log(int) override { std::cout << "derived int\n"; }
};
Derived d;
d.log(3.14); // "derived int" !!
Lookup starts in Derived, finds log(int), stops — Base::log(double) is invisible. 3.14 converts to int. Declaring any log in Derived hides all base overloads of that name. Hiding is per-name, not per-signature.
Fix:
struct Derived : Base {
using Base::log; // un-hides all base overloads
void log(int) override;
};
Through a base handle there's no problem: Base& r = d; r.log(3.14); → lookup in Base sees both overloads → picks log(double) → not overridden → "double".
struct Base { virtual void process(int); };
struct Derived : Base {
void process(long); // different signature: NOT an override
};
Base* p = new Derived();
p->process(42); // "base" — vtable slot still holds Base::process
Derived::process(long) is an unrelated function sharing the name. Lookup for p->process(42) starts in Base (static type of p), the derived function is never even visible, and the vtable slot for process(int) was never replaced. Classic silent typo: long vs int, missing const, & vs &&. The fix is the next section.
override and finalstruct Derived : Base {
void process(long) override; // COMPILE ERROR: does not override anything
void process(int) override; // OK, verified
void done() final; // no class below may override done()
};
struct Leaf final : Derived { }; // no class may derive from Leaf
override asserts "a base virtual with this exact signature exists." It converts Trap B from silent wrong behavior into a compile error. Put it on every overriding function, no exceptions. Omitting it is a code-review red flag.final on a function: no further overriding. final on a class: no deriving.final is also an optimization enabler — see §10.struct Base {
virtual void f() {}
~Base() {} // NOT virtual — bug incoming
};
struct Derived : Base {
std::string* data = new std::string(1000, 'x');
~Derived() { delete data; }
};
Base* p = new Derived();
delete p; // undefined behavior
delete p with a non-virtual destructor statically binds to ~Base(). ~Derived() never runs; data leaks.
Strictly this is undefined behavior (UB) per the standard — "leak" is the typical symptom, but say "UB" in the interview.
And: having a vtable doesn't save you — only functions declared virtual get vtable slots. The destructor call here never consults the vtable.
Rule: any class with virtual functions gets virtual ~Base() = default;. Since the vptr already exists, it's free — no size or speed cost you weren't already paying.
(Related rule: a base class should have either a public virtual destructor, or a protected non-virtual one — the latter for interface classes never deleted through the base.)
struct Base {
Base() { init(); } // calls Base::init, NOT the override
virtual void init() { std::cout << "base init\n"; }
virtual ~Base() = default;
};
struct Derived : Base {
void init() override { std::cout << "derived init\n"; }
};
Derived d; // prints "base init"
During Base::Base(), the object's dynamic type is Base: the vptr points at Base's vtable, and the Derived part doesn't exist yet. Dispatching to Derived::init() would touch uninitialized members, so the language dispatches to the constructor's own class. Destructors: same rule in reverse — by the time ~Base() runs, the Derived part is already destroyed.
std::terminate.Access is checked at compile time against the static type. Dispatch happens at runtime. They don't talk to each other.
struct Base {
virtual void run() { std::cout << "base\n"; }
virtual ~Base() = default;
};
struct Derived : Base {
private:
void run() override { std::cout << "derived\n"; }
};
Derived d;
Base& r = d;
r.run(); // COMPILES, prints "derived" — access checked against Base (public)
d.run(); // COMPILE ERROR — access checked against Derived (private)
Yes: through the base handle you just called a private function, legally. The same function is callable or not depending on which handle you hold.
This enables the non-virtual interface (NVI) idiom (a.k.a. template method pattern): the base class exposes a public non-virtual function that defines the fixed skeleton, and customization points are private virtuals:
class Widget {
public:
void draw() { validate(); doDraw(); clip(); } // fixed contract
private:
virtual void doDraw() = 0; // override me, but only I call you
};
Derived classes override doDraw; users can only enter through draw(). The base class keeps control of pre/post-conditions.
struct Shape {
virtual double area() const = 0; // pure virtual
virtual ~Shape() = default;
};
= 0 makes the function pure virtual; a class with any pure virtual is abstract — cannot be instantiated. Derived classes must implement all pure virtuals to become concrete.Shape::area() { ... } out of line), callable as Shape::area() explicitly. Rarely used; know it exists.Devirtualization = the compiler proving a virtual call has exactly one possible target, turning it into a direct (inlinable) call.
struct Base { virtual int f() { return 1; } virtual ~Base() = default; };
struct Derived final : Base { int f() override { return 2; } };
int a(Derived& d) { return d.f(); } // devirtualized — because of `final`
int b(Base& base) { return base.f(); } // stays virtual — could be anything
int c() {
Derived d;
Base& r = d;
return r.f(); // devirtualized — compiler sees d's construction
}
final, a Derived& might refer to some further-derived type that overrides f again — not safe to devirtualize. final (on the class, or on the specific function) guarantees no further override exists → direct call. final on hot-path leaf classes is a real optimization, not just a design statement.if (vptr == expected) inline_body(); else virtual_call();.std::vector<std::unique_ptr<Shape>> with 10M elements in a hot loop is slow beyond dispatch itself. The vector's pointers are contiguous but the objects are scattered across the heap → a cache miss per element, and the hardware prefetcher (which excels at streaming contiguous memory) can't predict pointer targets.
Layout hierarchy, best to worst for cache behavior:
| # | Layout | Example | Wins when |
|---|---|---|---|
| 1 | SoA — structure of arrays: one contiguous array per field | vector<float> radii; | Hot loops touch one or two fields across many objects. Every byte fetched is useful; loops vectorize (SIMD — single instruction, multiple data) trivially. |
| 2 | AoS — array of structures: whole objects contiguous | vector<Circle> | You touch all fields of an object together. The natural layout. |
| 3 | Array of pointers ("pointer soup"): contiguous pointers, scattered objects | vector<unique_ptr<Shape>> | Never wins on cache; only when you truly need runtime polymorphism per element. |
Fixes for the Shape problem, in order of impact:
vector<Circle>, vector<Rect>, sum each): contiguous objects, each loop monomorphic → devirtualized, inlined, vectorized. Moves you from layout 3 to 2; the devirtualization win usually exceeds the cache win.std::variant<Circle, Rect, ...> + std::visit: one container, objects inline by value. Middle ground; each element pays for the largest alternative.struct A { virtual void fa() {} int x; };
struct B { virtual void fb() {} int y; };
struct C : A, B { };
Layout — two vptrs:
Why can't they share one vptr? Code compiled against B knows nothing about C and expects a vptr at offset 0 from wherever the B* points. Each base subobject must be a valid object at its own address. (Single inheritance extends the base in place — one shared vptr — which is why this never came up before.)
C c;
A* pa = &c; // same address (A is at offset 0)
B* pb = &c; // compiler ADDS 16 — pb != (void*)&c
A derived-to-base conversion is not always a no-op — it can change the address. Consequences:
static_cast<C*>(pb) subtracts the offset back; the compiler knows both directions.reinterpret_cast or C casts through void* — they skip the adjustment and yield a garbage object.C overrides fb() and it's called through a B*, this must be adjusted from B* to C* before the body runs — the compiler inserts a thunk (tiny adjustment stub) in the vtable.Virtual inheritance (struct C : virtual A) solves the diamond problem (one shared A subobject when two bases both inherit A) at the cost of runtime-located offsets. Know it exists and what problem it solves; don't volunteer implementation details.
Sections 3 and 11 in one image. Two hierarchies:
struct Base { virtual void f(); virtual void g(); int x; };
struct D1 : Base { void f() override; int a; };
struct D2 : Base { void g() override; double b; };
struct A { virtual void fa(); int x; };
struct B { virtual void fb(); int y; };
struct C : A, B { void fb() override; int z; };
How to read it: the left column is per-object bytes (offsets and stored values, on the stack or heap at runtime); the right column is per-class vtables (fixed addresses in the executable's read-only data). An arrow means "the value stored in this vptr is that vtable's address." Addresses are illustrative.
What the diagram encodes — the summary of this whole guide's mechanics:
a at +12. D2 is 24: double b needs 8-byte alignment, so 4 bytes of padding sit at +12 to push b to +16. Same member count, different sizes — member alignment and ordering decide. (Layouts here follow the Itanium C++ ABI — application binary interface — used by GCC and Clang; the C++ standard itself doesn't specify vtables at all.)p->g() compiles to "call slot 1" without knowing the class. Indexed, not searched.&Base::g — inheritance was resolved when the compiler built the table, not at call time.Base* p = &d1 needs no adjustment and one vptr suffices.z comes after both. B can't sit at offset 0 (A is there), hence the second vptr, the +16 on pb, and the thunk that fixes this before entering C::fb.dynamic_cast and RTTI (brief)RTTI = run-time type information (the type data hanging off the vtable).
Base* p = ...;
if (auto* d = dynamic_cast<Derived*>(p)) { /* p really points to a Derived */ }
// pointer form: returns nullptr on failure
// reference form: dynamic_cast<Derived&>(r) throws std::bad_cast on failure
if Circle... else if Rect...) — that's what virtual functions are for. Legitimate uses: cross-casting in MI, plugin boundaries.typeid(*p) yields the dynamic type's std::type_info for polymorphic types.override on every overriding function. Always.virtual ~Base() = default; in any class with virtual functions (it's free — the vptr already exists).using Base::f; in the derived class when overriding one of several overloads.final on leaf classes/functions in hot paths = free devirtualization.variant over vector<unique_ptr<Base>> in hot loops.dynamic_cast chains = design smell; that's virtual dispatch's job.override → silent hiding, base version runsB* pb = &c changes the address; two vptrs in the objectfinal yes, plain Base& no