---
title: C++ Interview Questions & Answers (2026): OOP, STL & Memory
description: The C++ interview questions that get asked in 2026 — OOP, the STL, pointers, smart pointers, move semantics and the vtable — with clear, concrete answers.
url: https://usegreenroom.app/blog/cpp-interview-questions
last_updated: 2026-06-21
---

← Back to blog

Technical

# C++ interview questions and answers

June 21, 2026 · 33 min read

![C++ interview questions and answers — cover from Greenroom, the AI mock interviewer](/assets/blog/cpp-interview-questions-hero.webp)

You're forty minutes into a live coding round on a shared editor — the kind where you can see the interviewer's cursor blink while they read your code in real time. You've just written a clean little `Buffer` class with a raw pointer member, you `delete ptr;` in the destructor like a responsible adult, and then — because you're nervous and your fingers are doing things your brain hasn't approved — you scroll down and `delete ptr;` it again in a cleanup function you forgot you'd already written. The interviewer doesn't say anything. They just... look at the screen. Then at you. Then back at the screen, the way you'd look at a smoke detector that's beeping for no obvious reason.

"Walk me through what happens when this runs," they say, in the calm voice interviewers use right before something goes very wrong for you.

You know, technically, that double-freeing a pointer is undefined behavior — the heap allocator's internal bookkeeping gets corrupted, and the crash (if you're lucky enough to get a crash and not silent corruption) might not even happen on the line that caused it. But knowing the textbook phrase "undefined behavior" and being able to narrate, out loud, in real time, exactly *why* your own freshly-typed code just became a ticking time bomb are two completely different skills — and the gap between them is, more or less, the entire C++ interview.

This is the joke, but it is not really a joke: **C++ interview questions** are brutal specifically because the language refuses to protect you from yourself. Python and Java interviewers ask "do you understand how this works." C++ interviewers ask "do you understand how this works *and* what happens the instant you get it slightly wrong," because the language will cheerfully let you get it slightly wrong, compile fine, and detonate in production three weeks later. That's not a flaw interviewers are testing for out of cruelty — it's the actual job. Systems programming, trading infrastructure, game engines, and embedded/robotics roles all run on the assumption that the person writing this code understands ownership at the byte level, because there is no garbage collector standing behind them to clean up the mess.

This guide goes deep on the **C++ interview questions** that actually get asked in 2026 — object-oriented design and the vtable mechanism underneath it, memory management and the Rule of Three/Five, smart pointers and what they actually cost at runtime, the STL and its real complexity trade-offs, move semantics and why they exist, and a closing set of "what does this print" gotchas that separate people who've memorized definitions from people who've actually internalized them. Each section also covers how the verbal, follow-up-driven version of this prep differs from reading a definition silently — because in the real interview, nobody accepts "it's undefined behavior" as a complete answer. They want to know *why*.

## OOP in C++

### What are the four pillars of OOP, and how does C++ implement each one?

**Encapsulation** bundles data and the functions that operate on it inside a class, exposing a controlled interface via `public`/`private`/`protected` access specifiers — so callers can't reach in and corrupt internal state directly. **Abstraction** hides implementation detail behind a simpler interface, typically via abstract base classes with pure virtual functions, so callers depend on *what* a type does, not *how*. **Inheritance** lets a derived class reuse and extend a base class's members (`class Dog : public Animal`), modeling "is-a" relationships. **Polymorphism** lets code written against a base type work correctly with any derived type at runtime, via virtual functions — call `speak()` on an `Animal*` that actually points to a `Dog`, and `Dog::speak()` runs.

```cpp
class Animal {
public:
  virtual void speak() const { std::cout << "..."; }
  virtual ~Animal() = default;
};

class Dog : public Animal {
public:
  void speak() const override { std::cout << "Woof"; }
};
```

Interviewers ask this as a warm-up, but the real signal is whether you can name a concrete example of each from code you've actually written, not just recite the definitions. "I used inheritance once" is not an answer. "I had a `PaymentProcessor` base class with `StripeProcessor` and `RazorpayProcessor` subclasses, and the calling code never knew which one it had" is an answer.

### What is a virtual function, and how does the vtable work?

A virtual function is a member function declared with the `virtual` keyword that a derived class can override, enabling runtime (dynamic) polymorphism instead of compile-time dispatch. The compiler implements this by giving every class with virtual functions a hidden **vtable** — a per-class array of function pointers — and every *object* of that class a hidden pointer (the vptr, usually the object's first eight bytes on a 64-bit system) to its class's vtable.

When you call a virtual function through a base-class pointer or reference, the compiler emits code that follows the object's vptr to its actual class's vtable and calls the function pointer stored there, rather than the base class's version. That indirection — pointer to vptr, vptr to vtable, vtable slot to function — is "dynamic dispatch," and it's why virtual calls cost slightly more than non-virtual ones: one extra pointer dereference per call, and the compiler generally can't inline a virtual call because it doesn't know the concrete type at compile time. This is the mechanism that makes the following correctly call `Dog::speak()` even though the static type of `a` is `Animal*`:

```cpp
Animal* a = new Dog();
a->speak();   // vptr lookup -> Dog's vtable -> Dog::speak()
delete a;     // virtual destructor matters here too — see below
```

The follow-up interviewers love asking here: "what's the memory overhead of making a class polymorphic?" The honest answer is one pointer (the vptr) per object, plus one vtable per class (shared across all instances) — usually negligible, but not zero, which is exactly why some performance-critical embedded code deliberately avoids virtual functions in hot paths.

### Virtual vs pure virtual function — what's the difference, and what is an abstract class?

A regular virtual function has a body in the base class that derived classes *may* override; if they don't, the base implementation runs. A **pure virtual function** (`virtual void speak() const = 0;`) has no implementation in that class and *must* be overridden by any concrete derived class — it exists purely to define an interface. Any class with at least one pure virtual function is an **abstract class**: it can't be instantiated directly (`Animal a;` won't compile), but you can still have `Animal*` pointers and references that point to concrete derived objects.

```cpp
class Shape {
public:
  virtual double area() const = 0;   // pure virtual — no body
  virtual ~Shape() = default;
};

class Circle : public Shape {
  double r;
public:
  explicit Circle(double radius) : r(radius) {}
  double area() const override { return 3.14159265 * r * r; }
};
```

`Shape s;` fails to compile. `Shape* s = new Circle(2.0);` is fine. This is C++'s mechanism for forcing an interface contract — if you forget to implement a pure virtual function in a subclass, that subclass is itself abstract and the compiler stops you from instantiating it, catching the bug at compile time instead of at a 2am production incident.

### What is a virtual destructor, and why does it matter?

If a base class destructor isn't `virtual` and you `delete` a derived object through a base-class pointer, only the *base* class's destructor runs — the derived class's destructor is skipped entirely, leaking any resources it owned (open files, heap memory, locks). Marking the base destructor `virtual` makes destruction go through the vtable just like any other virtual call, so the correct derived destructor runs first, then the base destructor, in the proper order.

```cpp
class Base {
public:
  ~Base() { std::cout << "Base cleanup\n"; }      // NOT virtual — bug
};
class Derived : public Base {
  int* buffer = new int[1000];
public:
  ~Derived() { delete[] buffer; std::cout << "Derived cleanup\n"; }
};

Base* b = new Derived();
delete b;   // only "Base cleanup" prints — buffer leaks, silently
```

The rule interviewers expect you to state cleanly: **any class designed to be a polymorphic base class — one you intend to delete through a base pointer — needs a virtual destructor**, even if that destructor's body is empty. The C++ Core Guidelines state this explicitly as one of their class-design rules, and it's one of the few "always do this" rules in a language otherwise full of "it depends."

### Multiple inheritance and the diamond problem

C++ allows a class to inherit from more than one base class directly — `class Duck : public Flyable, public Swimmable {}`. This is genuinely useful for mixing independent capabilities, but it creates the classic **diamond problem**: if `Flyable` and `Swimmable` both inherit from a common `Animal` base, and `Duck` inherits from both, a naive compiler gives `Duck` *two separate copies* of `Animal`'s data — ambiguous, wasteful, and a source of real bugs (which `Animal::name` do you mean?).

```cpp
class Animal { public: std::string name; };
class Flyable : public Animal {};
class Swimmable : public Animal {};
class Duck : public Flyable, public Swimmable {};   // two Animal copies!

Duck d;
// d.name;  // ambiguous — compiler error: which Animal::name?
```

The fix is **virtual inheritance**: `class Flyable : public virtual Animal {}` and `class Swimmable : public virtual Animal {}` tell the compiler to share a single `Animal` base across the whole hierarchy, so `Duck` ends up with exactly one `Animal` subobject. The cost is a small amount of extra indirection (the offset to the shared base is no longer a compile-time constant, so it's resolved through a pointer at runtime) and noticeably more constructor-initialization complexity — the most-derived class becomes responsible for initializing the virtual base directly, even if it's several levels removed. Most C++ style guides, including the C++ Core Guidelines, recommend avoiding multiple inheritance of *state* entirely and reserving it for mixing in pure-interface (all-pure-virtual) base classes, where the diamond problem never arises because there's no data to duplicate.

### Function overloading vs overriding, and operator overloading

**Overloading** is having multiple functions with the *same name* but different parameter lists in the same scope, resolved at compile time based on the argument types you pass — `void print(int)` and `void print(std::string)` are overloads. **Overriding** is a derived class providing its own implementation of a *virtual* function inherited from a base class, resolved at runtime based on the object's actual type. They solve different problems: overloading is about ergonomics for callers; overriding is about polymorphic behavior.

**Operator overloading** lets you redefine what an operator means for a user-defined type:

```cpp
class Vector2D {
public:
  double x, y;
  Vector2D operator+(const Vector2D& other) const {
    return Vector2D{x + other.x, y + other.y};
  }
  friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
    return os << "(" << v.x << ", " << v.y << ")";
  }
};
```

It's idiomatic C++ as long as the overload does what a reader would expect — don't make `+` perform subtraction. The `<<` overload for `std::ostream` is the one nearly every C++ codebase defines, to make custom types printable in tests and logs without writing a separate `toString()`-style method.

## Pointers, references & memory management

### Pointer vs reference — what are the key differences?

A pointer is a variable that holds a memory address and can be reassigned, can be null, and needs explicit dereferencing (`*ptr`) to access the pointed-to value. A reference is an alias for an existing variable — it must be bound at initialization, can never be null or rebound to refer to something else, and is used with the same syntax as the variable itself, no dereferencing operator needed. The practical guidance interviewers want: prefer references for function parameters when you don't need null or reassignment (cleaner call-site syntax, no null checks needed), and reach for pointers (ideally smart pointers) when you genuinely need nullability, reassignment, or ownership semantics.

### `new`/`delete` vs `malloc`/`free`, and stack vs heap

`malloc`/`free` are C functions: they allocate raw, untyped memory and never call constructors or destructors. `new`/`delete` are C++ operators: `new T()` allocates memory *and* calls `T`'s constructor; `delete ptr` calls `ptr`'s destructor *and* frees the memory. Mixing them is a real bug — calling `free()` on memory from `new` (or vice versa) is undefined behavior because the two allocators don't share bookkeeping, and `free()` never runs your destructor, silently skipping cleanup for any resources the object owns.

**Stack** memory is allocated automatically for local variables, has a size known at compile time (or at least bounded), and is reclaimed automatically when the enclosing scope exits — extremely fast (it's just moving a stack pointer) but limited in size and lifetime. **Heap** memory is allocated explicitly (`new`, `malloc`) at runtime, can be arbitrarily large, and persists until explicitly freed — slower to allocate (the allocator has to find a suitable free block) and entirely your responsibility to release. The interview-ready framing: use the stack by default for anything whose lifetime matches its scope; reach for the heap when you need an object to outlive its creating scope, when the size isn't known until runtime, or when the object is large enough that copying it on the stack across function calls would be wasteful.

### What is RAII, and why is it central to idiomatic C++?

RAII (Resource Acquisition Is Initialization) ties a resource's lifetime to an object's lifetime: acquire the resource in the constructor, release it in the destructor, and let C++'s guaranteed stack-unwinding call that destructor automatically when the object goes out of scope — even if an exception is thrown partway through. This is *the* idiom that makes C++ memory-safe without garbage collection: `std::vector`, `std::string`, `std::lock_guard`, and every smart pointer are RAII wrappers around a resource (heap memory, a mutex, a file descriptor) that release it deterministically.

Here's the pattern applied to a simple file handle wrapper:

```cpp
class FileHandle {
  FILE* fp;
public:
  explicit FileHandle(const char* path) : fp(fopen(path, "r")) {}
  ~FileHandle() { if (fp) fclose(fp); }   // runs even if an exception unwinds the stack
  FileHandle(const FileHandle&) = delete;             // disable copy — see Rule of Three below
  FileHandle& operator=(const FileHandle&) = delete;
};
```

And here's the same idea applied to a mutex, which is arguably the single most common RAII pattern in real production C++:

```cpp
std::mutex mtx;

void update_shared_state() {
  std::lock_guard<std::mutex> lock(mtx);   // acquires the lock in the constructor
  // ... critical section ...
}   // lock_guard's destructor releases the mutex here — even if an exception is thrown above
```

No matter how the enclosing scope exits — normal return, early return, or exception — the destructor runs and the resource gets released. Interviewers ask this because forgetting RAII and managing resources manually with raw `new`/`delete`, `fopen`/`fclose`, or `lock`/`unlock` pairs is the single biggest source of leaks, double-frees, and deadlocks in real C++ code. "Why not just call `unlock()` manually at the end of the function?" is a real follow-up — the answer is that any early `return`, `break`, or thrown exception between the lock and your manual `unlock()` call leaves the mutex held forever, and `lock_guard` makes that bug structurally impossible.

### Shallow copy vs deep copy, and the Rule of Three / Rule of Five / Rule of Zero

A **shallow copy** copies a class's member values as-is, including raw pointers — so two objects end up pointing at the *same* heap memory, and when one is destroyed and frees it, the other is left holding a dangling pointer. A **deep copy** allocates new memory and copies the pointed-to data itself, so each object owns an independent copy.

```cpp
class Buggy {
  int* data;
public:
  Buggy(int val) : data(new int(val)) {}
  ~Buggy() { delete data; }
  // no custom copy constructor — compiler generates a shallow one
};

Buggy a(5);
Buggy b = a;     // shallow copy: b.data == a.data (same address!)
// when both a and b are destroyed, `delete data` runs TWICE on the same pointer
```

The compiler generates a default copy constructor, copy assignment operator, and destructor for you — but if your class manually manages a resource (a raw pointer, a file handle), those defaults do the wrong (shallow) thing. The **Rule of Three** says: if you write any one of destructor, copy constructor, or copy assignment, you almost certainly need to write all three, to keep deep-copy semantics consistent.

```cpp
class Safe {
  int* data;
public:
  Safe(int val) : data(new int(val)) {}
  ~Safe() { delete data; }
  Safe(const Safe& other) : data(new int(*other.data)) {}             // deep copy
  Safe& operator=(const Safe& other) {
    if (this == &other) return *this;
    *data = *other.data;
    return *this;
  }
};
```

The **Rule of Five** extends this for move semantics (C++11+): add the move constructor and move assignment operator too, so the class can also be moved efficiently instead of always deep-copying. The **Rule of Zero** is the modern punchline most candidates miss: in current C++, the cleanest answer is usually to avoid all five entirely by holding a `unique_ptr`/`shared_ptr` member instead of a raw pointer — the smart pointer's own correct copy/move semantics propagate automatically through the compiler-generated defaults, so the class needs none of the five written by hand. Interviewers specifically reward candidates who name the Rule of Zero unprompted, because it shows you know the *destination* the other rules are pointing toward, not just the rules themselves.

### Memory leaks and dangling pointers — what causes each, and how do you avoid them

A **memory leak** happens when heap memory is allocated and the program loses every pointer to it before freeing it — the memory is unreachable but never released, and it accumulates until the process runs out of address space. Common causes: forgetting a matching `delete`, an early `return`/thrown exception between `new` and `delete` (exactly what RAII fixes), and a `shared_ptr` reference cycle (covered below). A **dangling pointer** points to memory that has already been freed — dereferencing it is undefined behavior, and the bug is often silent at first because the freed memory might still happen to hold the old values, until something else allocates over it and the program corrupts in a way that's nearly impossible to trace back to the original `delete`.

```cpp
int* p = new int(42);
delete p;
std::cout << *p;   // dangling pointer dereference — undefined behavior, may "work" or may crash
p = nullptr;        // best practice: null it immediately after delete
```

The practical defenses interviewers want to hear, in order of how much they actually prevent in real codebases: prefer smart pointers and RAII so there's no manual `delete` to forget; null a raw pointer immediately after deleting it if you must keep one around; and run tools like Valgrind or AddressSanitizer in CI, because some leaks only show up under specific code paths that a human reviewer won't catch by reading the diff.

![C++ interview topics — OOP, STL, virtual functions, smart pointers](/assets/blog/pool-structured-screen.webp)

C++ rounds reward depth: virtual functions, the STL, and memory ownership.

## Smart pointers

### Explain smart pointers — `unique_ptr`, `shared_ptr`, and `weak_ptr`

Smart pointers are RAII wrappers around a raw pointer that automatically free the underlying memory when the right condition is met, eliminating most manual `delete` calls.

**`unique_ptr`** models exclusive ownership — only one `unique_ptr` can own a given object at a time, it can't be copied (only moved), and it frees the object the instant it goes out of scope. It's the default choice for heap allocation in modern C++, and it has effectively zero runtime overhead compared to a raw pointer — no atomic operations, no extra bookkeeping, just a destructor call that runs `delete`.

**`shared_ptr`** models shared ownership via a reference count stored in a separate "control block" allocated alongside (or near) the object — copying a `shared_ptr` atomically increments the count, destroying one atomically decrements it, and the object is freed when the count hits zero. That word *atomically* matters: the increment/decrement has to be thread-safe because two threads might copy or drop the same `shared_ptr` concurrently, and atomic operations are measurably slower than a plain integer increment — `shared_ptr` is not free, and reaching for it by default "just in case" has a real, measurable cost in hot paths.

**`weak_ptr`** is a non-owning reference to an object managed by a `shared_ptr` — it doesn't bump the reference count and doesn't keep the object alive. To actually use the object, you call `.lock()`, which returns a `shared_ptr` if the object is still alive or an empty one if it's already been freed — that check is exactly what makes `weak_ptr` safe to hold onto without risking a dangling-pointer dereference.

```cpp
std::unique_ptr<Widget> w = std::make_unique<Widget>();   // sole owner; freed at scope exit
std::shared_ptr<Widget> s1 = std::make_shared<Widget>();  // refcount = 1
std::shared_ptr<Widget> s2 = s1;                            // refcount = 2 (atomic increment)
std::weak_ptr<Widget> wk = s1;                               // doesn't bump refcount

if (auto locked = wk.lock()) {     // safe: returns nullptr-equivalent if Widget is gone
  locked->doSomething();
}
```

### When does `shared_ptr` leak memory anyway, and what is a reference cycle?

This is the follow-up interviewers ask immediately after you explain `shared_ptr` correctly, because it tests whether you actually understand reference counting or just memorized the phrase "automatically frees memory." The answer: a **reference cycle**. If object A holds a `shared_ptr` to object B, and object B holds a `shared_ptr` back to object A, each object's reference count never reaches zero — A keeps B alive, B keeps A alive, and neither is ever destroyed, even though nothing outside the cycle can reach either of them anymore.

```cpp
struct Node {
  std::shared_ptr<Node> next;
  std::shared_ptr<Node> prev;   // cycle: next and prev keep each other alive forever
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;
// even after a and b go out of scope, the cycle keeps both alive — a leak
```

The fix is to make one direction of the relationship non-owning: change `prev` to a `std::weak_ptr<Node>`. Now `b`'s back-reference to `a` doesn't bump `a`'s count, so when the last *owning* `shared_ptr` to `a` goes away, `a`'s count actually reaches zero and it gets destroyed — breaking the cycle. This is the canonical real-world use case for `weak_ptr`, and "describe a situation where you'd need one" is a question that separates candidates who've used `shared_ptr` in a toy example from candidates who've shipped it in a real codebase with a parent-child or observer relationship.

### When should you still use a raw pointer in modern C++?

Modern C++ guidance (per the C++ Core Guidelines) is blunt: raw pointers should mean **non-owning observation**, never ownership. If a function just needs to *look at* an object without taking any responsibility for its lifetime — and the caller guarantees the object outlives the call — a raw pointer (or, more idiomatically, a reference) is fine and actually clearer than a smart pointer, because passing a `shared_ptr` by value to a function that doesn't need to share ownership is a wasted atomic increment for no benefit. The rule of thumb interviewers want: `unique_ptr` for ownership by default, `shared_ptr` only when ownership is genuinely shared between multiple independent owners with unclear lifetimes, raw pointers/references for "I'm just borrowing this, I don't own it," and `weak_ptr` specifically to observe a `shared_ptr`-managed object without extending its life or creating a cycle.

## Move semantics

### Lvalues, rvalues, and rvalue references — what's actually being distinguished?

An **lvalue** is an expression that refers to a persistent object with an identifiable location in memory — a named variable, basically anything you could take the address of. An **rvalue** is a temporary that has no persistent identity beyond the current expression — the result of `a + b`, a literal `5`, or a temporary returned by value from a function. Before C++11, there was no way for a function to distinguish "you passed me a named object you still care about" from "you passed me a temporary that's about to be destroyed anyway" — both bound to the same `T&` parameter, so the only safe option was always to copy.

**Rvalue references** (`T&&`) let a function overload specifically on that distinction. `void f(T& x)` binds to lvalues; `void f(T&& x)` binds to rvalues. This is the entire foundation move semantics is built on: once the compiler can tell "this argument is a temporary nobody else will use again," a function is free to steal its internals instead of copying them, because there's no one left who'd notice the original being gutted.

### Move constructor and move assignment — what do they actually do, and why are they faster?

A **move constructor** takes a `T&&` and steals the source's internal pointers/buffers directly, then nulls out the source so its destructor doesn't double-free — turning what would be an O(n) deep copy into an O(1) pointer swap.

```cpp
class Buffer {
  int* data;
  size_t size;
public:
  Buffer(size_t n) : data(new int[n]), size(n) {}

  // Copy constructor: O(n) — allocates and copies every element
  Buffer(const Buffer& other) : data(new int[other.size]), size(other.size) {
    std::copy(other.data, other.data + size, data);
  }

  // Move constructor: O(1) — steals the pointer, leaves the source empty
  Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
    other.data = nullptr;
    other.size = 0;
  }

  // Move assignment: same idea, but must also release this object's own old data first
  Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
      delete[] data;
      data = other.data;
      size = other.size;
      other.data = nullptr;
      other.size = 0;
    }
    return *this;
  }

  ~Buffer() { delete[] data; }
};
```

`std::move(x)` doesn't itself move anything — it's just a cast (`static_cast<T&&>(x)`) that tells the compiler "treat `x` as an rvalue here," making it eligible to bind to the move constructor/assignment instead of the copy versions. After `std::move(x)`, `x` is left in a valid-but-unspecified state — you can still assign a new value to it or destroy it safely, but you should not read its old contents.

```cpp
std::vector<Buffer> v;
Buffer b(1'000'000);
v.push_back(b);              // copies b — allocates 1M ints again
v.push_back(std::move(b));   // moves b — steals the pointer, no new allocation
```

This matters in practice everywhere a large object crosses a function boundary by value: returning a `std::vector` or `std::string` from a function, inserting into a container, or passing ownership between subsystems. Before move semantics, all of these silently cost a full deep copy; after, they're pointer swaps, which is why move semantics is widely considered one of the most impactful performance changes the language has ever shipped, alongside the introduction of the STL itself.

### `noexcept` on move operations — why does it matter so much?

Marking a move constructor `noexcept` isn't just documentation — `std::vector` specifically checks at compile time whether a type's move constructor is `noexcept`, and if it is, `vector` uses moves when it needs to reallocate and relocate existing elements to a larger buffer. If the move constructor *isn't* marked `noexcept`, `vector` falls back to copying elements during reallocation instead, even if a perfectly good move constructor exists — because if a move threw partway through reallocation, `vector` couldn't guarantee the strong exception-safety guarantee it promises (the original buffer would already be partially destroyed). This is a genuinely surprising, high-signal interview answer: a missing `noexcept` can silently downgrade your supposedly-fast move constructor back into the copy path, with no compiler warning.

### Perfect forwarding — what problem does it solve?

When a template function needs to pass its argument through to another function without losing whether it was originally an lvalue or rvalue, a plain `T&&` parameter doesn't work, because inside the function body, even a parameter declared `T&&` is itself an lvalue (it has a name and a stable location) — forwarding it naively with `std::move` would always move, even when the caller passed an lvalue that shouldn't be moved from.

```cpp
template<typename T>
void wrapper(T&& arg) {
  inner(std::forward<T>(arg));   // preserves arg's original value category
}
```

`std::forward<T>(arg)` uses a compile-time trait (reference collapsing on the deduced template parameter `T`) to cast `arg` back to whatever value category the *caller* originally passed — lvalue stays lvalue, rvalue stays rvalue — so `inner` receives exactly the same kind of argument `wrapper` received. This is the mechanism behind `std::make_unique`, `std::make_shared`, and every generic factory/wrapper function in the standard library: they need to construct an object from arguments of arbitrary, unknown value category, without forcing an unnecessary copy or, worse, moving from something the caller still needed.

## The STL

### `vector` vs `list` vs `deque` — when do you reach for each?

| Container | Random access | Insert/erase at ends | Insert/erase in middle | Memory layout |
|---|---|---|---|---|
| `vector` | O(1) | O(1) amortized at back, O(n) at front | O(n) | contiguous |
| `deque` | O(1) | O(1) at both ends | O(n) | chunked, non-contiguous |
| `list` | O(n) | O(1) | O(1) given an iterator | per-node, non-contiguous |

`std::vector` stores elements contiguously, so random access is O(1) and iteration is cache-friendly — it's the right default container almost always. `std::list` is a doubly-linked list — insertion/removal anywhere is O(1) given an iterator to the spot, but random access is O(n) and each node's separate heap allocation makes iteration cache-unfriendly, which in practice often makes `list` slower than `vector` even for the "insert in the middle" workload it's supposedly optimized for, unless the elements are large and moves are expensive. `std::deque` (double-ended queue) supports O(1) push/pop at both ends, backed by a sequence of fixed-size chunks rather than one contiguous block — useful for a sliding-window or work queue where you add/remove from both ends.

The honest interview answer: default to `vector`, reach for `deque` when you need fast operations at both ends, and reach for `list` only when you've actually measured that pointer-stable O(1) middle-insertion beats `vector`'s cache locality for your workload — which is rarer than people assume, because modern CPUs are so much faster at sequential memory access than at chasing pointers across cache lines that `vector`'s "theoretically worse" O(n) insertion frequently outruns `list`'s "theoretically better" O(1) insertion in practice.

### `map` vs `unordered_map`; `set` vs `unordered_set` — what's the trade-off?

| Container | Backing structure | Lookup/insert/erase | Ordering |
|---|---|---|---|
| `map` / `set` | red-black tree | O(log n) | sorted by key |
| `unordered_map` / `unordered_set` | hash table | average O(1), worst case O(n) | none |

`std::map` and `std::set` are ordered, backed by a red-black tree, giving O(log n) lookup/insert/erase and the ability to iterate keys in sorted order or do range queries (`lower_bound`/`upper_bound`). `std::unordered_map` and `std::unordered_set` are backed by a hash table, giving average O(1) lookup/insert/erase but no ordering guarantee, and worst-case O(n) if many keys collide into the same bucket (a pathological but real concern if an attacker can choose keys to deliberately collide — some standard library implementations randomize the hash seed per process partly to defend against this).

The decision is straightforward: if you need sorted iteration or range queries, use the ordered version; if you just need fast key lookup and don't care about order, the unordered version is almost always faster in practice. The follow-up interviewers ask: "what does a key need to support for each?" — `map`/`set` need `operator<` (or a custom comparator); `unordered_map`/`unordered_set` need `operator==` and a `std::hash` specialization, which is exactly the gap you hit when you try to put a custom struct into an `unordered_map` and the compiler complains it doesn't know how to hash your type.

### What are iterators, and what is iterator invalidation?

An iterator is an object that abstracts "a position within a container" and supports moving to the next/previous element and dereferencing to get the value there, letting algorithms work uniformly across very different container layouts (`std::sort` works on anything providing random-access iterators, regardless of whether it's a `vector` or a raw array).

**Iterator invalidation** happens when a container operation makes existing iterators (or pointers/references into the container) no longer valid to use — for `std::vector`, calling `push_back` past current capacity reallocates the entire backing array, invalidating *every* iterator into the old buffer; calling `erase` invalidates iterators at or after the erased position. This is a classic real bug:

```cpp
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
  if (*it % 2 == 0) {
    v.erase(it);    // BUG: erase invalidates it, then ++it is undefined behavior
  }
}
```

The fix is always to reassign from `erase`'s return value, which points to the element that took the erased one's place:

```cpp
for (auto it = v.begin(); it != v.end(); ) {
  if (*it % 2 == 0) {
    it = v.erase(it);   // erase returns a valid iterator to the next element
  } else {
    ++it;
  }
}
```

`std::list` has gentler invalidation rules — erasing one node only invalidates the iterator to *that* node, not the whole container — which is one of the few genuinely strong arguments for `list` over `vector` when code elsewhere holds long-lived iterators into the middle of the container.

### What are templates, and what is template specialization?

A template lets you write a function or class once, parameterized over a type, and have the compiler generate a concrete version for each type you actually use it with at compile time — `std::vector<int>` and `std::vector<std::string>` are two entirely separate generated classes from the same `vector` template. This is how the STL achieves type safety and performance simultaneously: no runtime type checks or boxing, because the compiler knows the concrete type at every call site.

**Template specialization** lets you override the generic template's behavior for one specific type — for example, a generic `Container<T>` template might specialize `Container<bool>` to pack each bool into a single bit instead of using a full byte per element, exactly as `std::vector<bool>` does in the standard library (a well-known specialization with its own well-known quirks around returning proxy objects instead of real `bool&` references — `auto x = v[0];` for a `vector<bool>` does not give you a usable reference the way it would for any other `vector<T>`, which has tripped up enough people that it's now a standard "gotcha" question in its own right).

### Common STL algorithms worth knowing cold

`std::sort` (O(n log n), typically introsort — a hybrid of quicksort, heapsort, and insertion sort chosen at runtime based on input characteristics), `std::find` (O(n) linear scan for unsorted ranges; prefer `std::binary_search`/`lower_bound` — O(log n) — on sorted data), `std::accumulate` (folds a range into a single value, the STL's reduce), `std::transform` (applies a function to every element, writing results to an output range — the STL's map), and `std::unique` (removes consecutive duplicates, but only after the range is sorted, and it doesn't actually shrink the container — it returns an iterator to the new logical end, and you still need to call `erase` with that iterator to actually drop the trailing elements: the "erase-remove idiom," `v.erase(std::unique(v.begin(), v.end()), v.end());`, which trips up almost everyone the first time they see it).

<div class="verdict"><strong>The core truth:</strong> C++ interviews reward depth, not breadth. Knowing that virtual functions exist is table stakes; explaining <em>how</em> the vtable resolves a call, <em>why</em> a reference cycle leaks a <code>shared_ptr</code> pair, and <em>when</em> <code>vector</code> beats <code>list</code> despite the textbook claiming otherwise is what separates candidates. Surface-level C++ ("I know classes and vectors") cracks fast under a single follow-up question.</div>

## "What does this code print?" — common gotcha questions

C++ interviewers love a short snippet with a non-obvious output, because it tests whether you actually understand the mechanism rather than the vocabulary around it. Here are the ones that come up most.

### Object slicing

```cpp
class Animal {
public:
  virtual std::string speak() const { return "..."; }
};
class Dog : public Animal {
public:
  std::string speak() const override { return "Woof"; }
};

void printSpeak(Animal a) {   // passed BY VALUE, not by reference or pointer
  std::cout << a.speak();
}

Dog d;
printSpeak(d);   // prints "...", not "Woof"
```

This prints `...`, not `Woof` — a result that surprises almost everyone who hasn't seen it before. Passing `d` by value to a parameter typed `Animal` copies *only the `Animal` part* of `d`; the `Dog`-specific data and vtable pointer are sliced off entirely, leaving a plain `Animal` object that calls `Animal::speak()`. This is **object slicing**, and the fix is to take the parameter by reference or pointer (`void printSpeak(const Animal& a)`), which preserves the object's real type and lets the vtable lookup find `Dog::speak()` correctly. The deeper lesson interviewers want stated explicitly: polymorphism only works through pointers and references — passing or storing a polymorphic type by value silently breaks it, with no compiler warning.

### Calling a virtual function from a constructor or destructor

```cpp
class Base {
public:
  Base() { init(); }              // calling a virtual function from the constructor
  virtual void init() { std::cout << "Base::init "; }
};
class Derived : public Base {
public:
  void init() override { std::cout << "Derived::init "; }
};

Derived d;   // prints "Base::init " — NOT "Derived::init "
```

This prints `Base::init`, not `Derived::init`, which feels wrong if you've internalized "virtual calls always resolve to the most-derived override." During `Base`'s constructor, the `Derived` part of the object hasn't been constructed yet — its vtable pointer hasn't been set up to point at `Derived`'s vtable yet — so the object behaves, for the duration of `Base`'s constructor, as if it really were just a `Base`. The same applies in reverse during destructors: by the time `~Base()` runs, `Derived`'s members are already destroyed, so a virtual call inside `~Base()` resolves to `Base`'s version too. The practical guidance: never rely on virtual dispatch inside a constructor or destructor expecting derived behavior — if you need derived-specific initialization, do it via a separate `init()` method called after construction, not from inside the constructor itself.

### Static initialization order

```cpp
// file_a.cpp
extern int b;
int a = b + 1;   // is b already initialized at this point? Undefined.

// file_b.cpp
int b = 5;
```

Whether `a` ends up as `6` or `1` depends on which translation unit's static initializers run first — and across separate `.cpp` files, the standard makes **no guarantee** about that order. This is the "static initialization order fiasco," and it's a real bug class in large codebases, not just a trivia question. The standard fix is the **construct-on-first-use idiom**: wrap the static value in a function returning a reference to a local `static` variable, which C++ guarantees is initialized the first time that function is called, regardless of link order:

```cpp
int& getB() {
  static int b = 5;   // initialized on first call, guaranteed — no ordering ambiguity
  return b;
}
```

### What gets printed, and why — a post-increment gotcha

```cpp
int i = 0;
std::cout << i++ << " " << i++ << " " << i;
```

This one is technically undefined behavior in C++14 and earlier (the order of evaluation of `<<` operands relative to each other wasn't specified, so different compilers genuinely printed different things), and became well-defined left-to-right in C++17. Knowing that the rules *changed*, rather than confidently stating a single answer as if it always held, is itself the signal interviewers are listening for — it shows you know the standard evolves and you don't assume old StackOverflow answers still apply to the compiler you're using today.

## How C++ prep methods compare — and where Greenroom fits

Most candidates preparing for a C++ round reach for the same handful of resources, and each one trains a real but partial slice of what the interview actually tests.

**GeeksforGeeks-style C++ question dumps** are genuinely useful as a checklist of topics — they'll remind you that virtual destructors, the Rule of Five, and `map` vs `unordered_map` all exist. Their weakness is that reading "the answer" to "what is a virtual destructor" and being able to produce that answer cold, under a follow-up like "okay, but what if the destructor in the derived class also throws," are different skills, and a question dump never tests the second one. You can read fifty answers and still freeze the first time someone interrupts you mid-explanation.

**LeetCode grinding** sharpens algorithmic problem-solving and is genuinely necessary for the DS&A round of most C++ interviews, but it trains almost none of the OOP-design, memory-ownership, or "explain your reasoning out loud" muscle that C++-specific rounds lean on hardest. Plenty of strong LeetCode grinders get blindsided by "why did you choose `unique_ptr` here instead of `shared_ptr`" precisely because that question never appears on a judge that only checks whether your output matches expected output.

**Reading cppreference.com passively** is how most working C++ engineers actually learn the language day to day, and it's an excellent reference — but reading a page about `std::shared_ptr`'s reference-counting behavior is a fundamentally different activity from being asked, cold, by an interviewer, "why would this leak memory," with no page to scroll back to. Passive reading builds recognition; interviews test recall and synthesis under mild social pressure, which passive reading doesn't train at all.

**A friend's WhatsApp-forwarded "Top 50 C++ Interview Questions" PDF** is the lowest-effort, lowest-signal option on this list, and most candidates know it — but it's tempting precisely because it's free and feels like progress. The honest problem: those PDFs circulate for years, accumulate copy-paste errors (a wrong vtable explanation propagates across a dozen forwarded copies faster than anyone fact-checks it), and contain zero mechanism for finding out which of your own answers are actually wrong until an interviewer tells you, live, in the room that matters.

**ChatGPT-generated answer lists** are faster to produce than a GeeksforGeeks dump and can be tailored to a specific role or company, which is a real advantage — but the failure mode is identical to reading any other static list: you get a fluent, confident-sounding answer to read, not a chance to find out whether *you*, specifically, can reproduce that reasoning out loud without the text in front of you. It's also worth knowing that generated explanations of subtle mechanisms like vtable layout or move-constructor exception safety occasionally contain small but real inaccuracies that a static read-through won't catch — you'd only find out in front of an interviewer.

**Greenroom's** approach is different in kind, not just degree: it's a spoken mock interview that asks you to explain a concept like RAII or `unique_ptr` vs `shared_ptr` out loud, and then — critically — follows up on your answer the way a real interviewer would, asking "why not the other one" or "what if this threw an exception here" before moving on. That follow-up is the part every static resource above structurally cannot provide, because a PDF or a question dump doesn't know what you just said and can't push back on it. The honest tradeoff: a verbal mock interview won't teach you syntax you've genuinely never seen before — for that, cppreference.com and a textbook are still the right tool — but it's the only practice method on this list that surfaces the gap between *recognizing* a correct answer and *producing* one live, which is exactly the gap that sinks otherwise well-prepared candidates in the actual room. The two are complementary: read to learn the mechanism, then practice explaining the mechanism out loud until a follow-up question doesn't rattle you.

## How to prepare

C++ rounds probe the "why" behind features far more than syntax recall — an interviewer who hears "vtable" wants to know you can describe the pointer indirection, not just that the word exists. Practise explaining ownership transfer, virtual dispatch, and container trade-offs out loud, the same way you'd defend a design decision on a real team, because reciting a definition and reasoning through a follow-up ("what if the base destructor isn't virtual here?") are different skills.

A realistic prep sequence for a C++-heavy round, roughly two to three weeks out: spend the first week solidifying OOP and memory fundamentals (vtable mechanics, RAII, the Rule of Three/Five/Zero) until you can explain each one without notes; spend the second week on smart pointers and the STL complexity trade-offs, specifically practicing the "why this container and not that one" framing rather than just memorizing Big-O tables; and spend the final days running through "what does this print" snippets and full mock interviews, since that's where the gap between recognition and recall shows up fastest. Revisit our [data structures interview guide](/blog/data-structures-interview-questions) for the underlying complexity analysis these STL trade-offs build on, and our [OOPs guide](/blog/oops-interview-questions) for the design-pattern layer that often sits on top of a C++ low-level-design round.

[Greenroom](/) runs spoken technical mock interviews that ask realistic follow-ups on exactly this kind of reasoning and give feedback on how clearly you explained it, not just whether the final answer was right. If your C++ round includes a low-level design component (parking lot, elevator, library system), our [system design guide](/blog/system-design-interviews-what-they-test) covers the requirements-first framework interviewers expect, and [coding-interview communication tips](/blog/coding-interview-communication-tips) covers the verbal habits — narrating your thinking, stating assumptions out loud, handling a follow-up gracefully — that matter just as much as the C++ knowledge itself once you're actually in the room. Pair both with our [C programming guide](/blog/c-programming-interview-questions) for the lower-level pointer-and-memory angle C++ interviews often pull from directly.

## Frequently asked questions

### What are the most common C++ interview questions?

Common C++ questions cover OOP (the four pillars, virtual functions and the vtable, pure virtual functions, virtual destructors, multiple inheritance and the diamond problem, overloading vs overriding), memory and ownership (pointer vs reference, RAII, smart pointers, shallow vs deep copy, the Rule of Three/Five/Zero, memory leaks and dangling pointers), move semantics (lvalues vs rvalues, move constructors, std::move, perfect forwarding), and the STL (vector vs list vs deque, map vs unordered_map, iterators and iterator invalidation, common algorithms). Interviewers also frequently use short "what does this code print" snippets to test object slicing, virtual calls from constructors, and static initialization order.

### What is a virtual function in C++?

A virtual function is a member function declared with the virtual keyword that can be overridden in derived classes, enabling runtime polymorphism. When called through a base-class pointer or reference, the actual derived implementation runs, resolved via a per-class table of function pointers called the vtable. A virtual destructor is essential when deleting derived objects through base pointers, otherwise the derived destructor is skipped and resources leak.

### What are smart pointers in C++?

Smart pointers are RAII wrappers that manage dynamic memory automatically. unique_ptr provides exclusive ownership and frees the resource when it goes out of scope with effectively zero overhead versus a raw pointer; shared_ptr provides shared ownership via an atomically-updated reference count and frees the resource when the count hits zero, at the cost of that atomic overhead; and weak_ptr is a non-owning reference that breaks reference cycles between shared_ptrs. They prevent leaks and dangling pointers compared to raw new/delete.

### What is the difference between the Rule of Three and the Rule of Five?

The Rule of Three says that if a class manually manages a resource and needs a custom destructor, copy constructor, or copy assignment operator, it almost always needs all three, to avoid shallow-copy bugs like double-frees. The Rule of Five extends this for C++11 and later: add the move constructor and move assignment operator too, so the class can be moved cheaply (stealing internals) instead of always deep-copying. The Rule of Zero is the modern follow-up: in current C++, holding a smart pointer member instead of a raw pointer often avoids needing to write any of the five yourself, since the compiler-generated defaults already do the right thing.

### When should I use `vector` instead of `list` in an interview answer?

Default to vector almost always — contiguous storage gives O(1) random access and cache-friendly iteration, which dominates real-world performance even for workloads list is theoretically suited to. Reach for list only when you need guaranteed O(1) insertion/removal at arbitrary positions via an existing iterator and have actually measured that it beats vector for your specific access pattern, or when code elsewhere holds long-lived iterators into the middle of the container, since list's iterators survive erasure of unrelated elements. Reach for deque when you need fast push/pop at both ends.

### Why does object slicing happen, and how do I avoid it?

Object slicing happens when a derived object is passed, returned, or assigned by value into a variable or parameter typed as the base class — only the base-class portion of the object gets copied, the derived-specific data and the vtable pointer are discarded, and any virtual call on the sliced object resolves to the base class's version instead of the derived override. The fix is to always pass and store polymorphic types through pointers or references rather than by value, since slicing only happens on a true by-value copy.

### How should I prepare for a C++ interview?

Go deep on virtual functions and the vtable, RAII and smart pointers, the Rule of Three/Five/Zero and move semantics, and STL container trade-offs, since C++ interviews probe the "why" behind features rather than just the names. Practise explaining these out loud, ideally with a voice-based mock interview that follows up, because surface-level knowledge cracks quickly under a single follow-up question, and static resources like question dumps or read-only references can't simulate that follow-up at all.

C++ interviews reward depth explained out loud. Greenroom runs spoken technical interviews that follow up on your reasoning. Free to start.
