Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

OOP, Virtual & Destructors
C++

OOP, Virtual & Destructors

Practice 12 questions covering object-oriented programming, inheritance, polymorphism, virtual functions, vtables, and destructors.

1. What occurs if foo() is invoked inside B’s constructor during the instantiation of a derived class object D?

Answer: The base version B::foo() executes, not the derived override.

This is the “virtual calls don’t dispatch during construction” rule. The reason is initialization order. When you create a D, the base subobject B is constructed first. During B’s constructor, the object’s virtual table pointer (vptr) is set to B’s vtable — the derived class D hasn’t started constructing yet, and its members don’t exist.

So when B’s constructor calls the virtual foo(), the runtime looks up the vptr and finds B’s vtable. The call resolves to B::foo(). This is deliberate: calling D::foo() would execute code that may read D’s data members — which are not yet initialized (in fact, for data members they’d be uninitialized, and for the vptr of D they’re not even set). The language prevents this hazard by fixing the dynamic type to B for the duration of the base constructor.

The same rule applies in the destructor: during ~B, the vptr reverts to B’s vtable, and virtual calls resolve to the base version.

The interview answer: B::foo() runs, because the vptr points to the base vtable until the derived constructor begins — virtual dispatch during base construction resolves to the base.

Answer:

The base version B::foo() executes, not the derived override.

This is the “virtual calls don’t dispatch during construction” rule. The reason is initialization order. When you create a D, the base subobject B is constructed first. During B’s constructor, the object’s virtual table pointer (vptr) is set to B’s vtable — the derived class D hasn’t started constructing yet, and its members don’t exist.

So when B’s constructor calls the virtual foo(), the runtime looks up the vptr and finds B’s vtable. The call resolves to B::foo(). This is deliberate: calling D::foo() would execute code that may read D’s data members — which are not yet initialized (in fact, for data members they’d be uninitialized, and for the vptr of D they’re not even set). The language prevents this hazard by fixing the dynamic type to B for the duration of the base constructor.

The same rule applies in the destructor: during ~B, the vptr reverts to B’s vtable, and virtual calls resolve to the base version.

The interview answer: B::foo() runs, because the vptr points to the base vtable until the derived constructor begins — virtual dispatch during base construction resolves to the base.

2. What functionality does the mutable specifier provide when applied to a non-static class member variable?

Answer: It allows the member to be mutated inside const member functions.

The const on a member function makes the this pointer const-qualified: the function promises not to modify the object. A mutable member is exempt from that promise. Even in a const member function, a mutable member can be read and written.

Why would you want that? There are two classic cases:

  • Logical vs. bitwise constness: an object is logically const (its externally observable data never changes), but it has internal bookkeeping that needs updating. The canonical examples are a mutex (locking it must not require a non-const object) and a memoization cache (lazily computing and storing a result is fine even on a const object).
  • Reference counters or debugging statistics.

So mutable doesn’t add thread-safety and doesn’t change memory layout — it only relaxes the const restriction for one specific field. The interview answer: mutable permits modification of that member inside const member functions, used for things like mutexes and caches.

Answer:

It allows the member to be mutated inside const member functions.

The const on a member function makes the this pointer const-qualified: the function promises not to modify the object. A mutable member is exempt from that promise. Even in a const member function, a mutable member can be read and written.

Why would you want that? There are two classic cases:

  • Logical vs. bitwise constness: an object is logically const (its externally observable data never changes), but it has internal bookkeeping that needs updating. The canonical examples are a mutex (locking it must not require a non-const object) and a memoization cache (lazily computing and storing a result is fine even on a const object).
  • Reference counters or debugging statistics.

So mutable doesn’t add thread-safety and doesn’t change memory layout — it only relaxes the const restriction for one specific field. The interview answer: mutable permits modification of that member inside const member functions, used for things like mutexes and caches.

3. How does a C++ class containing virtual functions incur runtime memory and execution overhead?

Answer: Each instance carries a hidden virtual pointer (vptr) to a per-class virtual table (vtable), adding a pointer-sized memory cost and an indirection on each virtual call.

When a class declares at least one virtual function, the compiler adds a hidden pointer to each object: the vptr. All objects of the same class point to the same static vtable — an array of function pointers, one per virtual function, shared by the whole class.

Two costs:

  • Memory: one pointer per instance (typically 8 bytes), even if the class has no data members.
  • Dispatch: a virtual call is not a direct call. The runtime loads the vptr, indexes into the vtable, and calls through the function pointer — an extra indirection compared to a non-virtual call. In practice this is cheap, but it exists, and it also defeats some compiler inlining opportunities.

The trade-off buys runtime polymorphism: the same virtual call on a Base* can resolve to different derived implementations depending on the object’s actual type. The interview answer: a per-instance vptr to a shared static vtable — pointer-sized memory overhead per object plus an indirection on virtual dispatch.

Answer:

Each instance carries a hidden virtual pointer (vptr) to a per-class virtual table (vtable), adding a pointer-sized memory cost and an indirection on each virtual call.

When a class declares at least one virtual function, the compiler adds a hidden pointer to each object: the vptr. All objects of the same class point to the same static vtable — an array of function pointers, one per virtual function, shared by the whole class.

Two costs:

  • Memory: one pointer per instance (typically 8 bytes), even if the class has no data members.
  • Dispatch: a virtual call is not a direct call. The runtime loads the vptr, indexes into the vtable, and calls through the function pointer — an extra indirection compared to a non-virtual call. In practice this is cheap, but it exists, and it also defeats some compiler inlining opportunities.

The trade-off buys runtime polymorphism: the same virtual call on a Base* can resolve to different derived implementations depending on the object’s actual type. The interview answer: a per-instance vptr to a shared static vtable — pointer-sized memory overhead per object plus an indirection on virtual dispatch.

4. How does dynamic_cast signal failure when attempting an invalid downcast on pointer versus reference types?

Answer: A pointer cast returns nullptr; a reference cast throws std::bad_cast.

dynamic_cast performs a checked, runtime polymorphic cast (requires a polymorphic base — at least one virtual function). When the cast fails, the failure reporting depends on the target kind:

  • Pointer: dynamic_cast<T*>(p) returns nullptr. Callers check for null.
  • Reference: dynamic_cast<T&>(r) cannot return “nothing” — there’s no null reference in valid C++. So it throws std::bad_cast.

The asymmetry exists because references must always be valid; a failed reference cast has nowhere to put a “no result,” so it must raise an exception.

Base* b = new Derived;
if (Derived* d = dynamic_cast<Derived*>(b)) { /* ok */ }

Base& br = *b;
Derived& dr = dynamic_cast<Derived&>(br);  // throws std::bad_cast if fails

The interview answer: pointer → nullptr; reference → std::bad_cast exception.

Answer:

A pointer cast returns nullptr; a reference cast throws std::bad_cast.

dynamic_cast performs a checked, runtime polymorphic cast (requires a polymorphic base — at least one virtual function). When the cast fails, the failure reporting depends on the target kind:

  • Pointer: dynamic_cast<T*>(p) returns nullptr. Callers check for null.
  • Reference: dynamic_cast<T&>(r) cannot return “nothing” — there’s no null reference in valid C++. So it throws std::bad_cast.

The asymmetry exists because references must always be valid; a failed reference cast has nowhere to put a “no result,” so it must raise an exception.

Base* b = new Derived;
if (Derived* d = dynamic_cast<Derived*>(b)) { /* ok */ }

Base& br = *b;
Derived& dr = dynamic_cast<Derived&>(br);  // throws std::bad_cast if fails

The interview answer: pointer → nullptr; reference → std::bad_cast exception.

5. What is the size of an empty class struct in C++?

Answer: At least 1 byte (typically exactly 1).

An empty class has no data members, yet its objects must have a non-zero size. The reason is object identity: if empty objects were size 0, then two objects in an array could occupy the same address, and &a == &b would be true for distinct objects — which the standard forbids. Every object must have a distinct address.

So the compiler gives empty classes a minimum size of 1 byte. In arrays, each element gets its own byte, preserving distinct addresses.

A related optimization: the Empty Base Optimization (EBO) — an empty base class can take up no additional space when combined into a derived class. But a standalone empty object is at least 1 byte. The interview answer: non-zero, at least 1 byte, so distinct objects have distinct addresses.

Answer:

At least 1 byte (typically exactly 1).

An empty class has no data members, yet its objects must have a non-zero size. The reason is object identity: if empty objects were size 0, then two objects in an array could occupy the same address, and &a == &b would be true for distinct objects — which the standard forbids. Every object must have a distinct address.

So the compiler gives empty classes a minimum size of 1 byte. In arrays, each element gets its own byte, preserving distinct addresses.

A related optimization: the Empty Base Optimization (EBO) — an empty base class can take up no additional space when combined into a derived class. But a standalone empty object is at least 1 byte. The interview answer: non-zero, at least 1 byte, so distinct objects have distinct addresses.

6. Which keyword prevents implicit type conversions during single-argument constructor calls?

Answer: explicit.

A single-argument (or defaulted-with-args) constructor normally acts as an implicit conversion from its parameter type: passing an int where a String is expected would silently construct a String from the int. That implicit conversion is often surprising and sometimes dangerous (it can hide bugs or enable accidental conversions).

Marking the constructor explicit forbids implicit conversions:

  • String(int) — implicit conversion allowed: String s = 5; compiles.
  • explicit String(int) — only explicit construction: String s(5); or String s = String(5); compiles; String s = 5; is an error.

explicit applies to constructors and conversion operators, and it’s used to require callers to state their intent. The interview answer: explicit prevents implicit single-argument conversions, forcing explicit construction.

Answer:

explicit.

A single-argument (or defaulted-with-args) constructor normally acts as an implicit conversion from its parameter type: passing an int where a String is expected would silently construct a String from the int. That implicit conversion is often surprising and sometimes dangerous (it can hide bugs or enable accidental conversions).

Marking the constructor explicit forbids implicit conversions:

  • String(int) — implicit conversion allowed: String s = 5; compiles.
  • explicit String(int) — only explicit construction: String s(5); or String s = String(5); compiles; String s = 5; is an error.

explicit applies to constructors and conversion operators, and it’s used to require callers to state their intent. The interview answer: explicit prevents implicit single-argument conversions, forcing explicit construction.

7. What is the output of the following inheritance constructor sequence?

struct A {
    A() { std::cout << "A"; }
};
struct B : A {
    B() { std::cout << "B"; }
};
int main() {
    B b;
}

Output: AB.

Object construction is bottom-up in the inheritance hierarchy: base class constructors run first, then derived. The base must be fully constructed (its invariants established) before the derived part can be initialized.

So for B b;:

  1. A’s constructor runs first, printing A.
  2. Then B’s constructor body runs, printing B.

Output: AB.

The mirror rule applies to destruction, in reverse: derived destructor runs first, then base destructor (so the derived part is still intact when base cleanup runs). The interview answer: AB — base constructs first.

Answer:

AB.

Object construction is bottom-up in the inheritance hierarchy: base class constructors run first, then derived. The base must be fully constructed (its invariants established) before the derived part can be initialized.

So for B b;:

  1. A’s constructor runs first, printing A.
  2. Then B’s constructor body runs, printing B.

Output: AB.

The mirror rule applies to destruction, in reverse: derived destructor runs first, then base destructor (so the derived part is still intact when base cleanup runs). The interview answer: AB — base constructs first.

8. What problem occurs if a base class destructor is NOT declared virtual when deleting a derived class object through a base class pointer?

Base* ptr = new Derived();
delete ptr;

Answer: Only the Base destructor runsDerived’s destructor is never called — leaking resources owned by Derived and invoking undefined behavior.

delete works through the static type of the pointer you delete. If the base class destructor is non-virtual, the compiler has no way to dispatch to Derived’s destructor at runtime, so it calls Base::~Base() directly. Derived’s destructor (and any cleanup of members it allocated) is skipped — a resource leak, and per the standard, deleting through a non-virtual base destructor is undefined behavior.

struct Base { ~Base() {} };            // NON-virtual
struct Derived : Base { std::vector<int> v; };

Base* p = new Derived();
delete p;   // Derived::~Derived() never runs → v leaks

The rule of thumb: any class intended as a base class should have a virtual destructor (and if it’s a polymorphic base with virtual functions, it virtually always should). If a class is never meant to be derived from, mark it final and keep the destructor non-virtual.

The interview answer: with a non-virtual base destructor, delete runs only Base::~Base(), skipping Derived cleanup and causing UB + leaks.

Answer:

Only the Base destructor runsDerived’s destructor is never called — leaking resources owned by Derived and invoking undefined behavior.

delete works through the static type of the pointer you delete. If the base class destructor is non-virtual, the compiler has no way to dispatch to Derived’s destructor at runtime, so it calls Base::~Base() directly. Derived’s destructor (and any cleanup of members it allocated) is skipped — a resource leak, and per the standard, deleting through a non-virtual base destructor is undefined behavior.

struct Base { ~Base() {} };            // NON-virtual
struct Derived : Base { std::vector<int> v; };

Base* p = new Derived();
delete p;   // Derived::~Derived() never runs → v leaks

The rule of thumb: any class intended as a base class should have a virtual destructor (and if it’s a polymorphic base with virtual functions, it virtually always should). If a class is never meant to be derived from, mark it final and keep the destructor non-virtual.

The interview answer: with a non-virtual base destructor, delete runs only Base::~Base(), skipping Derived cleanup and causing UB + leaks.

9. What is the structural purpose of a Non-Virtual Interface (NVI) idiom in class design?

Answer: Public non-virtual functions enforce invariants and pre/post-conditions, delegating the actual work to private/protected virtual functions that derived classes override.

The NVI idiom flips the usual design. Instead of making the interface itself virtual, you make the public entry point non-virtual and non-overridable, and make a hook virtual:

class Base {
public:
    void doWork() {           // public non-virtual
        lock();               // common pre-conditions
        doWorkImpl();         // virtual hook
        unlock();             // common post-conditions
    }
protected:
    virtual void doWorkImpl() {}   // derived overrides this
};

Benefits: the base class controls when and around what the virtual does — locking, invariant checks, logging, validation — and derived classes can’t accidentally skip those steps by overriding the public function. The interface is stable; the extension points are contained. The interview answer: public non-virtual methods enforce common behavior (locks, checks) and delegate to private/protected virtual hooks.

Answer:

Public non-virtual functions enforce invariants and pre/post-conditions, delegating the actual work to private/protected virtual functions that derived classes override.

The NVI idiom flips the usual design. Instead of making the interface itself virtual, you make the public entry point non-virtual and non-overridable, and make a hook virtual:

class Base {
public:
    void doWork() {           // public non-virtual
        lock();               // common pre-conditions
        doWorkImpl();         // virtual hook
        unlock();             // common post-conditions
    }
protected:
    virtual void doWorkImpl() {}   // derived overrides this
};

Benefits: the base class controls when and around what the virtual does — locking, invariant checks, logging, validation — and derived classes can’t accidentally skip those steps by overriding the public function. The interface is stable; the extension points are contained. The interview answer: public non-virtual methods enforce common behavior (locks, checks) and delegate to private/protected virtual hooks.

10. What design safety guarantee does the final specifier provide when appended to a class member method declaration?

Answer: It prevents derived classes from overriding that specific virtual function.

Marking a virtual function final locks it: no class further down the hierarchy may override it. Attempting to do so is a compile-time error.

class Base {
    virtual void foo() final;   // no override allowed below
};
class Derived : public Base {
    void foo() override;        // ERROR: Base::foo is final
};

The benefit is twofold: it documents design intent (“this behavior is locked”), and it enables devirtualization — since the compiler knows the function can’t be overridden, it can sometimes resolve the call statically instead of through the vtable. final can also be applied to a whole class (no one may derive from it). The interview answer: final prevents further overriding of that virtual function, enabling devirtualization.

Answer:

It prevents derived classes from overriding that specific virtual function.

Marking a virtual function final locks it: no class further down the hierarchy may override it. Attempting to do so is a compile-time error.

class Base {
    virtual void foo() final;   // no override allowed below
};
class Derived : public Base {
    void foo() override;        // ERROR: Base::foo is final
};

The benefit is twofold: it documents design intent (“this behavior is locked”), and it enables devirtualization — since the compiler knows the function can’t be overridden, it can sometimes resolve the call statically instead of through the vtable. final can also be applied to a whole class (no one may derive from it). The interview answer: final prevents further overriding of that virtual function, enabling devirtualization.

11. What happens when a pure virtual function (virtual void foo() = 0;) is declared in a class?

Answer: The class becomes abstract and cannot be instantiated directly.

= 0 after a virtual function declaration marks it pure. A class with at least one pure virtual function is abstract: you can’t create objects of it. It can still be used as a base class, and derived classes must provide concrete implementations of all pure virtual functions to become instantiable themselves.

Note: a pure virtual function can have a body in C++ (you can still define it and call it explicitly from derived classes), but the class remains abstract. Abstract classes serve as interfaces/contracts — they define the shape of behavior without being usable as concrete types. The interview answer: declaring a pure virtual function makes the class abstract — no direct instantiation, derived classes must override all pure virtuals to be concrete.

Answer:

The class becomes abstract and cannot be instantiated directly.

= 0 after a virtual function declaration marks it pure. A class with at least one pure virtual function is abstract: you can’t create objects of it. It can still be used as a base class, and derived classes must provide concrete implementations of all pure virtual functions to become instantiable themselves.

Note: a pure virtual function can have a body in C++ (you can still define it and call it explicitly from derived classes), but the class remains abstract. Abstract classes serve as interfaces/contracts — they define the shape of behavior without being usable as concrete types. The interview answer: declaring a pure virtual function makes the class abstract — no direct instantiation, derived classes must override all pure virtuals to be concrete.

12. What design restriction applies to static class member functions in C++?

Answer: Static member functions cannot be const, volatile, or virtual, and have no this pointer — so they can’t access non-static members.

A static member function belongs to the class type, not to any instance:

  • No this pointer — there’s no instance to be “current.” Consequently it can only access static data members and call other static functions.
  • Can’t be const/volatile — those qualifiers describe the implicit this object, which doesn’t exist.
  • Can’t be virtual — virtual dispatch is an instance mechanism; a static function isn’t invoked through an object.

Use static methods for behavior that doesn’t depend on instance state: factories, utilities, and operations on static data. The interview answer: no this, no non-static member access, and they can’t be const, volatile, or virtual.

Answer:

Static member functions cannot be const, volatile, or virtual, and have no this pointer — so they can’t access non-static members.

A static member function belongs to the class type, not to any instance:

  • No this pointer — there’s no instance to be “current.” Consequently it can only access static data members and call other static functions.
  • Can’t be const/volatile — those qualifiers describe the implicit this object, which doesn’t exist.
  • Can’t be virtual — virtual dispatch is an instance mechanism; a static function isn’t invoked through an object.

Use static methods for behavior that doesn’t depend on instance state: factories, utilities, and operations on static data. The interview answer: no this, no non-static member access, and they can’t be const, volatile, or virtual.

My Private Notes

Notes are auto-saved locally to this device.