C++ interviews go deeper than most language interviews — interviewers expect you to understand memory management, the object lifecycle, undefined behavior, template mechanics, and concurrency primitives. This guide covers the questions that separate C++ practitioners from true experts.
Memory Management and RAII
What is RAII and why does C++ depend on it?
RAII (Resource Acquisition Is Initialization) ties a resource lifetime to an object lifetime. The constructor acquires the resource; the destructor releases it. Since destructors run deterministically when an object goes out of scope (including during stack unwinding from exceptions), RAII makes resource leaks structurally impossible.
#include <fstream>
#include <stdexcept>
void process(const std::string& path) {
std::ifstream file(path); // RAII: file opens in constructor
if (!file) throw std::runtime_error("cannot open");
// ... use file ...
} // RAII: file.close() called automatically — even if exception thrown
Without RAII you need explicit cleanup on every exit path (normal return, every throw, every early return). That is error-prone and does not compose.
When do you use unique_ptr vs shared_ptr vs raw pointers?
| Type | Ownership | Overhead | Use When |
|---|---|---|---|
unique_ptr |
Exclusive | Zero (same as raw) | Single owner, transfer via move |
shared_ptr |
Shared (ref-counted) | Atomic ref-count, heap control block | Multiple owners, unclear lifetime |
weak_ptr |
Non-owning observer | Same as shared_ptr minus ownership | Break shared_ptr cycles, cache |
| Raw pointer/ref | Non-owning borrow | Zero | Function parameters that do not own |
// Prefer unique_ptr by default
auto widget = std::make_unique<Widget>(args);
// shared_ptr when lifetime is genuinely shared
auto shared = std::make_shared<Widget>(args);
std::weak_ptr<Widget> weak = shared; // observe without extending lifetime
// Pass by raw pointer/ref — does not convey ownership
void render(const Widget* w); // borrows, does not own
Move Semantics
What is the difference between copy and move semantics?
A copy duplicates the resource. A move transfers ownership of the resource — the source is left in a valid but unspecified state (typically null/empty). Move semantics eliminate unnecessary copies for temporaries and return values.
#include <vector>
#include <string>
#include <utility>
std::vector<int> make_data() {
std::vector<int> v(1000000);
// ... fill v ...
return v; // NRVO or implicit move — no copy
}
std::string a = "hello";
std::string b = a; // copy — a still valid
std::string c = std::move(a); // move — a is now empty
// Move constructor: O(1), just pointer swap
// Copy constructor: O(n), allocates and copies all elements
What are the Rule of Three and Rule of Five?
Rule of Three (pre-C++11): if you define any of destructor, copy constructor, copy assignment — define all three. They exist when you manage a resource manually.
Rule of Five (C++11+): also define move constructor and move assignment. Or use the Rule of Zero: use RAII types (smart pointers, std containers) so the compiler-generated defaults work correctly and you define none.
class Buffer {
char* data_;
size_t size_;
public:
Buffer(size_t n) : data_(new char[n]), size_(n) {}
~Buffer() { delete[] data_; }
// Copy
Buffer(const Buffer& o) : data_(new char[o.size_]), size_(o.size_) {
std::copy(o.data_, o.data_ + o.size_, data_);
}
Buffer& operator=(const Buffer& o) {
if (this != &o) {
delete[] data_;
data_ = new char[o.size_];
size_ = o.size_;
std::copy(o.data_, o.data_ + o.size_, data_);
}
return *this;
}
// Move
Buffer(Buffer&& o) noexcept : data_(o.data_), size_(o.size_) {
o.data_ = nullptr; o.size_ = 0;
}
Buffer& operator=(Buffer&& o) noexcept {
if (this != &o) { delete[] data_; data_ = o.data_; size_ = o.size_;
o.data_ = nullptr; o.size_ = 0; }
return *this;
}
};
Templates and Type System
What is template specialization and when do you use it?
// Primary template
template<typename T>
struct Serializer {
static std::string serialize(const T& v) {
return std::to_string(v);
}
};
// Full specialization for std::string
template<>
struct Serializer<std::string> {
static std::string serialize(const std::string& v) {
return """ + v + """;
}
};
// Partial specialization for std::vector<T>
template<typename T>
struct Serializer<std::vector<T>> {
static std::string serialize(const std::vector<T>& v) {
std::string r = "[";
for (auto& el : v) r += Serializer<T>::serialize(el) + ",";
return r + "]";
}
};
What is SFINAE and when is it used vs concepts (C++20)?
SFINAE (Substitution Failure Is Not An Error): when template argument substitution fails, the compiler silently discards that overload instead of raising an error. Used for compile-time conditional overloads.
// SFINAE (C++11/14/17 style)
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T val) { /* integers only */ }
// Concepts (C++20) — cleaner, better error messages
template<std::integral T>
void process(T val) { /* integers only */ }
// Or with requires clause
template<typename T>
requires std::integral<T>
void process(T val) { }
Concurrency
What is a data race and how do you prevent it?
A data race occurs when two threads access the same memory location concurrently, at least one access is a write, and there is no synchronization. Data races are undefined behavior in C++ — the compiler may reorder or optimize away operations in ways that seem impossible.
#include <atomic>
#include <mutex>
#include <thread>
// WRONG: data race
int counter = 0;
// thread 1: counter++;
// thread 2: counter++; // UB
// FIX 1: atomic (for simple operations)
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed); // thread-safe
// FIX 2: mutex (for compound operations)
std::mutex mu;
int value = 0;
{
std::lock_guard<std::mutex> lock(mu);
value = value * 2 + 1; // read-modify-write is atomic under lock
}
What is the difference between std::mutex and std::shared_mutex?
std::mutex: exclusive lock — only one thread can hold it at a time (readers block each other). std::shared_mutex (C++17): supports shared (read) and exclusive (write) locks. Multiple readers can hold it simultaneously; a writer gets exclusive access.
#include <shared_mutex>
#include <unordered_map>
class Cache {
std::unordered_map<std::string, std::string> data_;
mutable std::shared_mutex mu_;
public:
std::string get(const std::string& key) const {
std::shared_lock lock(mu_); // shared — multiple readers OK
auto it = data_.find(key);
return it != data_.end() ? it->second : "";
}
void set(const std::string& key, const std::string& val) {
std::unique_lock lock(mu_); // exclusive — blocks all readers
data_[key] = val;
}
};
Undefined Behavior
What are common sources of undefined behavior in C++?
- Null/dangling pointer dereference: accessing memory through an invalid pointer
- Signed integer overflow:
INT_MAX + 1is UB (not wraparound — use unsigned or check first) - Out-of-bounds array access:
arr[n]where n >= size - Use after free: accessing memory after
delete - Data races: concurrent unsynchronized access
- Uninitialized reads: reading a local variable before writing
- Strict aliasing violations: accessing an object through a pointer of incompatible type
// Signed overflow — UB, compiler may optimize assuming it does not happen
int x = INT_MAX;
if (x + 1 > x) { ... } // compiler may eliminate this branch entirely
// Fix: use unsigned, or check before
if (x < INT_MAX) { ... x + 1 ... }
Modern C++ Features (C++17 / C++20)
| Feature | What It Does | Example |
|---|---|---|
| Structured bindings (C++17) | Unpack tuples/structs | auto [k,v] = *map.begin(); |
std::optional (C++17) |
Nullable value without pointer | optional<int> find(); |
std::variant (C++17) |
Type-safe union | variant<int,string> v = 42; |
if constexpr (C++17) |
Compile-time branch | Replaces SFINAE in many cases |
| Concepts (C++20) | Constrain templates | template<std::integral T> |
| Coroutines (C++20) | Stackless async | co_await async_read(); |
| Ranges (C++20) | Composable algorithms | views::filter | views::transform |
| Modules (C++20) | Replace header files | import std.core; |
Common Interview Questions
- What is the vtable and how does virtual dispatch work? Each class with virtual functions has a vtable (array of function pointers). Each object has a vptr (pointer to its class vtable). Virtual call = dereference vptr, index into vtable, indirect call. Cost: 1 extra pointer dereference + prevents inlining.
- What is placement new? Constructs an object at a specified memory address without allocating. Used in memory pools, arena allocators, and low-latency systems.
- What does
constmean on a member function? The implicitthispointer becomesconst T*— you cannot modify non-mutablemembers. Enables calling on const objects and const references. - What is the difference between
std::threadandstd::async?std::threadalways creates a new OS thread; you join manually.std::asyncwithlaunch::asyncruns on a thread pool (implementation-defined) and returns a future. For short tasks,asyncmay be more efficient; for long-running tasks, explicit threads give more control.
Frequently Asked Questions
What is RAII and why is it important in C++?
RAII (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime: the constructor acquires the resource (opens a file, allocates memory, locks a mutex) and the destructor releases it. Because C++ calls destructors deterministically when objects go out of scope — including during stack unwinding from exceptions — RAII makes resource leaks structurally impossible without extra cleanup code. std::unique_ptr, std::shared_ptr, std::lock_guard, and std::fstream are all RAII wrappers. Without RAII, you need explicit cleanup on every exit path, which is error-prone and does not compose across nested function calls.
What is the difference between unique_ptr and shared_ptr?
unique_ptr expresses exclusive ownership — only one unique_ptr can own an object at a time. Transfer of ownership is via std::move; copying is deleted. Zero overhead compared to raw pointers. shared_ptr expresses shared ownership — multiple shared_ptrs can point to the same object; a reference count tracks the owners. When the last shared_ptr is destroyed, the object is deleted. shared_ptr has overhead: heap-allocated control block, atomic reference count operations. Use unique_ptr by default; use shared_ptr only when ownership is genuinely shared. Prefer passing by raw pointer or reference for non-owning function parameters.
What is undefined behavior in C++ and why is it dangerous?
Undefined behavior (UB) means the C++ standard places no requirements on the program. The compiler is free to assume UB never occurs and optimize accordingly — eliminating null checks, assuming signed integers do not overflow, or reordering memory accesses. Common UB: null/dangling pointer dereference, signed integer overflow, out-of-bounds array access, use-after-free, data races, and uninitialized reads. UB is dangerous because it produces no guaranteed error — the program may appear to work in debug builds and fail silently or catastrophically in optimized builds. Use AddressSanitizer (-fsanitize=address), UBSanitizer (-fsanitize=undefined), and ThreadSanitizer (-fsanitize=thread) to detect UB during testing.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is RAII and why is it important in C++?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “RAII (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime: the constructor acquires the resource (opens a file, allocates memory, locks a mutex) and the destructor releases it. Because C++ calls destructors deterministically when objects go out of scope — including during stack unwinding from exceptions — RAII makes resource leaks structurally impossible without extra cleanup code. std::unique_ptr, std::shared_ptr, std::lock_guard, and std::fstream are all RAII wrappers. Without RAII, you need explicit cleanup on every exit path, which is error-prone and does not compose across nested function calls.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between unique_ptr and shared_ptr?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “unique_ptr expresses exclusive ownership — only one unique_ptr can own an object at a time. Transfer of ownership is via std::move; copying is deleted. Zero overhead compared to raw pointers. shared_ptr expresses shared ownership — multiple shared_ptrs can point to the same object; a reference count tracks the owners. When the last shared_ptr is destroyed, the object is deleted. shared_ptr has overhead: heap-allocated control block, atomic reference count operations. Use unique_ptr by default; use shared_ptr only when ownership is genuinely shared. Prefer passing by raw pointer or reference for non-owning function parameters.”
}
},
{
“@type”: “Question”,
“name”: “What is undefined behavior in C++ and why is it dangerous?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Undefined behavior (UB) means the C++ standard places no requirements on the program. The compiler is free to assume UB never occurs and optimize accordingly — eliminating null checks, assuming signed integers do not overflow, or reordering memory accesses. Common UB: null/dangling pointer dereference, signed integer overflow, out-of-bounds array access, use-after-free, data races, and uninitialized reads. UB is dangerous because it produces no guaranteed error — the program may appear to work in debug builds and fail silently or catastrophically in optimized builds. Use AddressSanitizer (-fsanitize=address), UBSanitizer (-fsanitize=undefined), and ThreadSanitizer (-fsanitize=thread) to detect UB during testing.”
}
}
]
}