C++ Interview Preparation: Top 35 Questions
Master the most common C++ interview questions with clear explanations and code examples.
C++ Interview Preparation: Top 35 Questions
These are the questions that come up repeatedly in C++ systems, game, and infrastructure engineering interviews. Each answer focuses on the “why” — the understanding interviewers actually probe for. Knowing the syntax is table stakes; being able to explain the design decisions and tradeoffs is what separates a strong candidate.
Memory Management
1. What is RAII?
RAII (Resource Acquisition Is Initialization) ties a resource’s lifetime to an object’s lifetime. The resource is acquired in the constructor and released in the destructor. Because destructors run deterministically when an object goes out of scope — even when exceptions are thrown — resources are never leaked regardless of how the scope is exited. This is the foundational safety pattern of modern C++.
class FileHandle {
public:
explicit FileHandle(const char* path) : fp(fopen(path, "r")) {
if (!fp) throw std::runtime_error("Cannot open file");
}
~FileHandle() { fclose(fp); } // guaranteed cleanup — no matter how we exit
FILE* get() { return fp; }
private:
FILE* fp;
};
2. Explain the Rule of Five
If you define any of: destructor, copy constructor, copy assignment, move constructor, move assignment — you probably need all five, because the compiler’s defaults will be wrong for a class that directly manages a resource. The compiler-generated copy constructor just copies the pointer, leading to double-free when both objects are destroyed.
class Buffer {
public:
explicit Buffer(size_t n) : data(new int[n]), size(n) {}
~Buffer() { delete[] data; }
Buffer(const Buffer& o) : data(new int[o.size]), size(o.size)
{ std::copy(o.data, o.data+size, data); }
Buffer& operator=(const Buffer& o) { Buffer tmp(o); swap(tmp); return *this; }
Buffer(Buffer&& o) noexcept : data(o.data), size(o.size)
{ o.data = nullptr; o.size = 0; }
Buffer& operator=(Buffer&& o) noexcept { swap(o); return *this; }
private:
void swap(Buffer& o) noexcept {
std::swap(data, o.data); std::swap(size, o.size);
}
int* data;
size_t size;
};
3. What is the difference between stack and heap?
The stack is managed automatically — frames are pushed when functions are called and popped when they return. Allocation is just a pointer decrement, making it essentially free. The stack is limited (typically 1–8 MB per thread), so large objects don’t belong there. The heap is manually managed (new/delete or malloc/free), essentially unlimited in size, but allocation involves bookkeeping overhead, potential OS calls, and you must track ownership manually.
16. What is the difference between new/delete and malloc/free?
new calls the constructor; delete calls the destructor. malloc/free only allocate and deallocate raw memory — no construction or destruction. Never mix them: don’t free something allocated with new, or delete something from malloc. In modern C++, prefer smart pointers over either.
17. What is placement new?
Placement new constructs an object at a pre-allocated memory address. It is used for memory pools, embedded systems, and custom allocators where you want to control exactly where objects are placed.
alignas(MyClass) char buffer[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass(args); // construct in-place — no heap allocation
obj->~MyClass(); // explicit destructor call required before reuse
Object Model
4. What is a vtable and how does virtual dispatch work?
When a class has virtual functions, the compiler creates a vtable — a static array of function pointers, one per virtual function. Each object of that class contains a hidden vptr (typically 8 bytes) pointing to the class’s vtable. A virtual call dereferences the vptr, indexes into the vtable, and calls the function pointer. Cost: one extra indirection per call — typically 1–3 nanoseconds, negligible except in tight inner loops.
12. What is the difference between shallow and deep copy?
A shallow copy copies pointer values — both the original and the copy point to the same underlying data. A deep copy allocates new memory and copies the pointed-to data. The compiler-generated copy constructor does a shallow copy, which is why raw-pointer-owning classes need a custom copy constructor that performs a deep copy.
13. What is a dangling reference?
A reference or pointer to memory that has been freed or gone out of scope. Accessing it is undefined behavior — the memory may have been reused for something else entirely.
int* dangling() {
int x = 42;
return &x; // x destroyed on return — pointer dangles
}
std::string& bad() {
std::string s = "hello";
return s; // UB: reference to local variable that no longer exists
}
18. Explain the diamond problem and virtual inheritance
When two base classes share a common ancestor and a class derives from both, without virtual inheritance there are two copies of the grandparent’s data, making member access ambiguous. Virtual inheritance makes the grandparent a single shared subobject.
struct Animal { int age; };
struct Dog : virtual Animal {};
struct Cat : virtual Animal {};
struct DogCat : Dog, Cat {}; // one Animal::age, unambiguous
19. What does the mutable keyword do?
Allows a member to be modified inside a const member function. Used for caches, mutexes, and lazy initialization where the modification is an implementation detail that doesn’t change the logical state of the object.
class Expensive {
public:
int compute() const {
if (!cached) { cache = heavyWork(); cached = true; }
return cache;
}
private:
mutable int cache = 0;
mutable bool cached = false;
};
20. What is an inline function?
inline suggests to the compiler to substitute the function body at call sites. Modern compilers inline aggressively based on their own heuristics. The keyword’s primary use today is allowing function definitions in headers to avoid ODR violations — each translation unit gets its own copy, and inline tells the linker that’s intentional.
Templates and Metaprogramming
10. What is SFINAE?
Substitution Failure Is Not An Error. When template argument substitution fails, the compiler silently discards that overload instead of raising an error. This enables compile-time conditional overloads based on type properties. It is the pre-C++20 mechanism for what Concepts do more cleanly.
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> half(T x) { return x / 2; }
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T> half(T x) { return x * 0.5; }
21. What is a template specialization?
Providing a specific implementation for particular template arguments when the generic implementation is wrong or inefficient for that type.
template <typename T> struct TypeName { static const char* get() { return "unknown"; } };
template <> struct TypeName<int> { static const char* get() { return "int"; } };
template <> struct TypeName<double> { static const char* get() { return "double"; } };
// Partial specialization — specialize for a family of types
template <typename T> struct TypeName<std::vector<T>> {
static std::string get() { return "vector<" + std::string(TypeName<T>::get()) + ">"; }
};
22. Explain std::enable_if and SFINAE
std::enable_if<condition, T>::type is only defined when condition is true. When false, the ::type member doesn’t exist, causing substitution failure that silently removes the overload from consideration. C++20 Concepts are the cleaner replacement — they produce readable error messages instead of substitution failure walls.
33. What are C++20 Concepts?
Named constraints on template parameters that produce clear error messages and serve as documentation. A concept is a predicate over types evaluated at compile time.
template <typename T>
concept Sortable = requires(T& t) {
std::begin(t); std::end(t);
{ *std::begin(t) < *std::begin(t) } -> std::convertible_to<bool>;
};
template <Sortable T>
void sortIt(T& container) { std::sort(std::begin(container), std::end(container)); }
Move Semantics and Value Categories
7. What is move semantics? Why is it important?
Move semantics allow transferring resources from a temporary (rvalue) instead of copying them. A move constructor “steals” the source’s heap buffer by taking its pointer and nulling the source, leaving it valid-but-empty. This turns O(n) copies into O(1) transfers for containers and strings — critical for returning large objects from functions and inserting into containers efficiently.
8. Explain std::move vs std::forward
std::move unconditionally casts to an rvalue reference — it doesn’t move anything itself, it just enables the move constructor or assignment to be called. After std::move(x), you should treat x as if its value is unspecified.
std::forward conditionally casts: preserves lvalue-ness if the original was an lvalue, and rvalue-ness if it was an rvalue. Used in perfect forwarding to pass arguments to another function without losing their value category.
template <typename T>
void wrapper(T&& arg) {
realFunction(std::forward<T>(arg)); // preserves value category — lvalue stays lvalue, rvalue stays rvalue
}
11. Explain copy elision and RVO/NRVO
The compiler is allowed — and since C++17, required in some cases — to construct a return value directly in the caller’s stack frame, skipping the copy or move entirely. Named Return Value Optimization (NRVO) applies when you return a named local; RVO applies to temporary returns. The result: return HeavyObject(args); typically allocates zero times for the copy. Do not add std::move to a return of a local variable — it defeats elision.
Smart Pointers
9. What is a smart pointer? When to use each type?
Smart pointers are RAII wrappers that express ownership semantics explicitly in the type system, making ownership transfer and lifetime management unambiguous.
unique_ptr — sole ownership. Zero overhead vs raw pointer. Use by default.
shared_ptr — shared ownership via reference counting. Use when lifetime is shared.
weak_ptr — non-owning observer of a shared_ptr. Breaks cycles; must lock() before use.
auto u = std::make_unique<Widget>(); // sole owner
auto s = std::make_shared<Widget>(); // shared
std::weak_ptr<Widget> w = s; // observe without extending lifetime
if (auto locked = w.lock()) { // safely access only if still alive
locked->render();
}
Undefined Behavior
5. What is undefined behavior? Give examples.
UB is behavior the C++ standard makes no guarantee about. The compiler is free to assume UB never happens and optimize accordingly, which can produce completely unexpected results — UB is not just “implementation-defined,” it means the program is invalid and anything can happen.
Common examples:
- Signed integer overflow:
INT_MAX + 1 - Out-of-bounds array access:
arr[n]where n >= size - Use after free / dangling pointer dereference
- Null pointer dereference
- Uninitialized variable read
- Data race (two threads, one write, no synchronization)
Concurrency
14. Explain memory order in std::atomic
Memory orders control how atomic operations synchronize with other threads. They specify which writes are visible to which reads and in what order. The default (seq_cst) provides total ordering across all threads — the safest and most predictable, at a small performance cost.
std::atomic<int> x{0};
x.store(1, std::memory_order_relaxed); // no sync, just atomicity — for pure counters
x.store(1, std::memory_order_release); // all prior writes visible to acquiring thread
x.load(std::memory_order_acquire); // sees all writes before paired release
x.store(1, std::memory_order_seq_cst); // full sequential consistency (default)
Use seq_cst until profiling shows it’s a bottleneck. Relaxed and acquire/release are tricky to use correctly.
15. What is a data race?
Two threads access the same memory location concurrently, at least one is a write, and there is no synchronization between them. Data races are undefined behavior in C++ — not just a logical error. Even if the value looks correct at runtime, the compiler and CPU are free to reorder operations around a data race in ways that break any assumption you make. Fix with std::atomic, std::mutex, or message passing.
STL Internals
24. What is the difference between std::vector and std::list?
vector stores elements contiguously — O(1) random access, cache-friendly iteration, O(n) insertion or removal in the middle. list is a doubly-linked list — O(1) insertion or removal anywhere with a valid iterator, but O(n) access and poor cache behavior because each node is a separate heap allocation. In practice, vector wins almost always because cache effects dominate. Use list only when you need stable iterators across insertions and deletions throughout the container.
25. When would you use std::map vs std::unordered_map?
map is a red-black tree: O(log n) operations, sorted iteration, stable iterators. unordered_map is a hash table: O(1) average operations, no ordering, faster for large maps. Use unordered_map when you need fast lookup and don’t need ordering. Use map when you need sorted traversal, range queries with lower_bound/upper_bound, or guaranteed worst-case O(log n).
Modern C++ Features
6. Difference between const and constexpr
const means the variable cannot be modified after initialization — the value may be determined at runtime. constexpr means the value must be computable at compile time, enabling its use in array sizes, template arguments, and switch cases.
const int n = rand(); // ok: runtime const — value unknown at compile time
constexpr int m = 42; // compile-time constant
constexpr int sq = m * m; // computed at compile time — 1764
// constexpr int bad = rand(); // error: rand() is not a constant expression
23. What is a lambda? What is a closure?
A lambda is an anonymous function object. A closure is the lambda plus the captured variables — the compiler generates an anonymous struct with operator() and member variables for the captures. Captures can be by value [=], by reference [&], or mixed [x, &y].
int threshold = 10;
auto isAbove = [threshold](int x) { return x > threshold; };
// isAbove is a closure — it holds a copy of 'threshold' as a member
std::vector<int> v{5, 12, 3, 18, 7};
v.erase(std::remove_if(v.begin(), v.end(), [&](int x) { return x < threshold; }),
v.end());
26. What is std::optional?
A wrapper that either contains a value or is empty (std::nullopt). Replaces sentinel values (-1, nullptr, empty string) with an explicit, type-safe “no value” — the type itself communicates that absence is a valid outcome.
std::optional<int> divide(int a, int b) {
if (b == 0) return std::nullopt;
return a / b;
}
27. What is std::variant?
A type-safe union that holds exactly one of a fixed set of types at a time. Eliminates raw unions and tagged-union boilerplate. Access via std::get<T> (throws if wrong type) or std::visit (exhaustive pattern matching).
28. What is a consteval function?
A consteval function must be evaluated at compile time — calling it with runtime arguments is a compile error. Stronger than constexpr, which can run at either compile or runtime.
consteval int square(int n) { return n * n; }
constexpr int x = square(5); // ok: compile-time
// int y = square(rand()); // error: must be a constant expression
29. What is std::span?
A non-owning view over a contiguous sequence. The correct parameter type when a function needs to read or write a range without caring whether it’s a vector, array, or raw pointer — eliminates the need for overloads.
void sum(std::span<const int> data); // accepts vector, array, C array — one function
30. Explain structured bindings
Decompose aggregates, tuples, and pairs into named variables in a single declaration. The names bind to the actual members, so taking by reference gives a reference to the original. Primary use: cleaner map iteration without .first/.second.
31. What is the spaceship operator?
operator<=> returns a comparison category (strong_ordering, weak_ordering, partial_ordering) encoding the three-way relationship between two objects. Define it once (or = default it) and the compiler generates all six relational operators.
32. What is if constexpr?
A compile-time if inside a template. The discarded branch is not instantiated at all, so it can contain code that would be ill-formed for other types — enabling clean template branching without SFINAE.
34. What is a coroutine?
A function that can suspend its execution and be resumed later. C++20 coroutines use co_yield (suspend and produce a value), co_await (suspend until an async operation completes), and co_return (final return). They enable generators, async I/O, and cooperative multitasking without threads.
35. What is the PIMPL idiom?
PIMPL (Pointer to IMPLementation) stores private implementation details in a separately-allocated object pointed to by a unique_ptr. The public header only sees a forward declaration. Changing private members only recompiles the .cpp, not every consumer of the header — this dramatically speeds up large builds and stabilizes the ABI.