The 60 C++ questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
C++ is a compiled, statically typed, multi-style language created by Bjarne Stroustrup, first released in 1985 as an extension of C. It keeps C's low-level access to memory and hardware while adding classes, templates, exceptions, and a large standard library. The design goal is zero-overhead abstraction: you don't pay at runtime for features you don't use, which is why C++ powers game engines, browsers, databases, and trading systems where every microsecond counts. In interviews, C++ questions probe memory management (pointers, references, RAII, ownership), the object model (constructors, destructors, virtual functions), and modern features (smart pointers, move semantics, templates), because that's where the language rewards understanding and punishes guesswork. This page collects the 60 questions that come up most, each with a direct answer and runnable code. The community-maintained cppreference.com is the canonical language reference when you want to check a detail; increasingly the first C++ round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: C++ in 100 Seconds
Video: C++ in 100 Seconds (Fireship, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your C++ certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
C++ is a compiled, statically typed language built as an extension of C that supports procedural, object-oriented, and generic styles. It adds classes, templates, exceptions, and a standard library while keeping C's direct access to memory and hardware.
It's used where performance and control matter: game engines, web browsers, databases, operating systems, embedded firmware, and low-latency trading. The design principle is zero-overhead abstraction, so you don't pay at runtime for features you don't use.
Key point: A one-line definition plus two concrete domains (games, systems) beats a feature list. Interviewers open with this to hear how you frame an answer.
Watch a deeper explanation
Video: C++ Tutorial for Beginners - Full Course (freeCodeCamp.org, YouTube)
C is a procedural language with no built-in classes, inheritance, or exceptions. C++ adds object-oriented programming, templates, RAII, references, function and operator overloading, and a much larger standard library, while staying mostly compatible with C so that most C code compiles as C++ with minor changes.
The practical difference is abstraction. C gives you functions and structs; C++ gives you classes with automatic cleanup, generic code through templates, and safer resource handling, all without giving up low-level control.
| Feature | C | C++ |
|---|---|---|
| Classes and objects | No | Yes |
| Templates / generics | No | Yes |
| Exceptions | No | Yes |
| RAII / destructors | No | Yes |
| Standard containers | No | Yes (vector, map, etc.) |
Key point: Don't say C++ is just C with classes. RAII and templates matters.
C++ is compiled: source goes through preprocessing, compilation to object code, and linking into a native executable that the CPU runs directly. There's no interpreter or virtual machine sitting between your program and the hardware, unlike Python or Java. That direct-to-machine model is what gives C++ its speed and its predictable performance.
That's why C++ is fast and why errors show up at compile time instead of runtime. The cost is a build step and platform-specific binaries, but for performance-sensitive work that trade is the point.
The fundamental types are int, char, bool, float, and double, plus void for no value. Modifiers like short, long, signed, and unsigned change the size and range, so long long int holds a wider range than int. The exact sizes are implementation-defined with minimum guarantees the standard sets.
For fixed-width needs, use the types from <cstdint> like int32_t and uint64_t, because relying on int always being 32 bits is a portability bug waiting to happen.
#include <cstdint>
int count = 42;
double price = 19.99;
bool active = true;
char grade = 'A';
int64_t big = 9000000000; // fixed width, portableA pointer is a variable that stores the memory address of another object. You get an address with &, and you access the pointed-to value by dereferencing with *. A pointer that points to nothing should be nullptr.
Pointers let you share and modify data without copying, build dynamic structures like linked lists, and manage heap memory. They're also the source of most C++ bugs: dangling pointers, null dereferences, and leaks, which is why modern code prefers smart pointers.
int value = 10;
int* p = &value; // p holds the address of value
*p = 20; // change value through the pointer
// value is now 20
int* empty = nullptr; // points to nothingKey point: Interviewers watch whether you say nullptr (modern) or NULL/0 (dated). Small tells like that signal how current your C++ is.
Watch a deeper explanation
Video: POINTERS in C++ (The Cherno, YouTube)
A reference is an alias for an existing object. It's bound once at initialization, can't be reseated, and can't be null in well-defined code. A pointer is a separate variable holding an address that can be reassigned, set to nullptr, and dereferenced.
Prefer references when the thing must always refer to a valid object and never changes target, like a function parameter you don't want copied. Use pointers when you need to represent absence (nullptr) or rebind to different objects.
int a = 1, b = 2;
int& ref = a; // ref is now another name for a
ref = b; // this assigns b's VALUE to a; ref still aliases a
int* ptr = &a; // ptr points to a
ptr = &b; // now ptr points to b (reseated)| Pointer | Reference | |
|---|---|---|
| Can be null | Yes (nullptr) | No |
| Can be reseated | Yes | No |
| Needs dereference (*) | Yes | No, used directly |
| Must be initialized | No | Yes |
Key point: The follow-up is usually 'which would you use for a function parameter?'. Have the by-reference-to-avoid-copy answer ready.
Watch a deeper explanation
Video: REFERENCES in C++ (The Cherno, YouTube)
Pass by value copies the argument, so the function works on its own copy and the caller's variable is untouched. Pass by reference gives the function an alias to the caller's object, so changes are visible and nothing is copied. Pass by pointer is similar but the argument can be nullptr and must be dereferenced.
For large objects you don't intend to modify, pass by const reference: no copy, no accidental mutation. That's the default choice the question expects for anything bigger than a machine word.
void byValue(int x) { x = 99; } // caller unchanged
void byRef(int& x) { x = 99; } // caller changed
void byConstRef(const std::string& s) { } // no copy, read-only
int n = 1;
byValue(n); // n == 1
byRef(n); // n == 99new allocates an object on the heap and returns a pointer to it; delete frees that memory and runs the destructor. For arrays you use new[] and the matching delete[]. Every new needs exactly one delete, or you leak memory.
In modern C++ you rarely call these directly. Smart pointers and containers manage heap memory for you, so raw new/delete in application code is usually a sign the ownership design needs work.
int* p = new int(5); // heap allocation
delete p; // free it, or leak
int* arr = new int[10]; // array form
delete[] arr; // must use delete[]Key point: Mixing new with delete[] (or vice versa) is undefined behavior. Naming that pairing rule shows you understand the mechanism, not just the keywords.
Stack memory holds local variables and function call frames. It's fast, automatically reclaimed when a scope ends, and limited in size. Heap memory is allocated on demand with new (or malloc), lives until you free it, and is much larger but slower to allocate.
Prefer the stack: objects clean themselves up and there's nothing to leak. Reach for the heap when an object must outlive the scope that created it or when the size isn't known at compile time.
| Stack | Heap | |
|---|---|---|
| Lifetime | Automatic (scope-bound) | Manual (until freed) |
| Speed | Very fast | Slower |
| Size | Small, fixed limit | Large |
| Cleanup risk | None | Leaks if not freed |
const marks something as read-only and lets the compiler enforce it. It applies to variables (const int max = 10), function parameters (const T&), member functions that promise not to modify the object, and pointers in two positions with different meanings.
const int* p is a pointer to a const int (can't change the value); int* const p is a const pointer (can't reseat it). Const-correctness catches accidental mutation at compile time and documents intent, which reviewers read as care.
int x = 5;
const int* p1 = &x; // can't change *p1, can reseat p1
int* const p2 = &x; // can change *p2, can't reseat p2
int size() const; // member fn won't modify the objectA reference is declared with & in the type and must be initialized to an existing object: int& r = x;. From then on r is just another name for x, so reading or writing r reads or writes x. There's no separate reference object to reseat.
The common uses are function parameters (avoid copies, allow modification) and return values (return a reference to an existing object, never to a local). Returning a reference to a local variable is a classic dangling-reference bug.
A C-style array has a fixed size known at compile time and no built-in size tracking or bounds safety. std::vector is a dynamic array from the standard library that manages its own memory, grows as you add elements, knows its own size, and frees itself when it goes out of scope.
Prefer std::vector in almost all C++ code. It gives you RAII, .size(), .at() with bounds checking, and iterators, while raw arrays leave you managing length and lifetime by hand.
int raw[3] = {1, 2, 3}; // fixed size, no .size()
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4); // grows automatically
size_t n = v.size(); // knows its lengthstd::string is the standard library's dynamic string type. It manages its own memory, resizes automatically, knows its length, and offers methods for concatenation, searching, and substrings. A char array (C-string) is a fixed buffer terminated by a null character with none of that safety.
Use std::string for anything but the lowest-level code. It removes buffer-overflow risk and manual length tracking, and it converts to a C-string with .c_str() when a legacy API needs one.
#include <string>
std::string name = "Ada";
name += " Lovelace"; // safe concatenation
size_t len = name.length();
const char* c = name.c_str(); // for C APIsstd::cout writes to standard output using the insertion operator <<, and std::cin reads from standard input using the extraction operator >>. They're defined in <iostream> and live in the std namespace. std::endl inserts a newline and flushes the buffer.
Chaining works because each << and >> returns the stream. For performance in loops, prefer '\n' over std::endl to skip the flush, and check the stream state after reading to catch bad input.
#include <iostream>
int age;
std::cout << "Enter age: ";
std::cin >> age;
std::cout << "You are " << age << '\n';The only language-level difference is default access: struct members are public by default, while class members are private by default. The same default applies to inheritance, public for struct and private for class. Everything else, methods, constructors, inheritance, access specifiers, is available to both, so the choice is really about convention.
By convention, struct is used for plain data aggregates with public fields, and class is used when there's encapsulation and behavior. That convention communicates intent even though the compiler treats them almost identically.
A constructor runs when an object is created and sets up its initial state; it has the class name and no return type, and can be overloaded. A destructor runs when the object is destroyed and releases resources; it's the class name prefixed with ~ and takes no arguments.
Destructors are the heart of RAII: acquire a resource in the constructor, release it in the destructor, and cleanup happens automatically on scope exit, even during an exception. That pairing is why C++ rarely needs try/finally.
class File {
public:
File(const char* path) { /* open */ }
~File() { /* close automatically */ }
};
void work() {
File f("data.txt");
} // ~File() runs here, even if an exception was thrownEncapsulation means bundling data with the functions that operate on it and hiding the internal state behind a controlled interface. C++ supports it with access specifiers: private members are hidden, public members form the interface, and protected members are visible to derived classes.
The point is that callers depend on the public interface, not the internal representation, so you can change the internals later without breaking them. Getters and setters, or better, meaningful methods, guard invariants that raw public fields can't.
public members are accessible from anywhere that can see the object. private members are accessible only inside the class itself and its declared friends. protected members are accessible inside the class and its derived classes, but not from outside code. The specifier applies to every member listed under it until the next one.
Default to private for data and expose behavior through public methods. Use protected sparingly, because it couples derived classes to the base's internals and makes the base harder to change.
| Specifier | Same class | Derived class | Outside |
|---|---|---|---|
| public | Yes | Yes | Yes |
| protected | Yes | Yes | No |
| private | Yes | No | No |
Function overloading lets several functions share a name as long as their parameter lists differ in number or types. The compiler picks the right one at compile time based on the arguments you pass. Return type alone can't distinguish overloads.
It's useful for operations that make sense on different types, like a print that takes an int, a double, or a string. Overload resolution is a compile-time mechanism, so there's no runtime cost.
void print(int n) { /* ... */ }
void print(double d) { /* ... */ }
void print(const std::string& s) { /* ... */ }
print(42); // calls int version
print(3.14); // calls double versionA default argument is a value the compiler supplies when the caller omits that parameter. They're declared in the function declaration and must come after all non-default parameters, because arguments are matched left to right.
Put the default in the declaration (usually the header), not the definition, to avoid duplicate-default errors. Defaults reduce overload count, but too many can hide which parameters callers actually care about.
void greet(const std::string& name, const std::string& greeting = "Hello") {
std::cout << greeting << ", " << name << '\n';
}
greet("Sam"); // Hello, Sam
greet("Sam", "Hi"); // Hi, SamA namespace groups related names to prevent collisions, so two libraries can both define a Timer without clashing. The standard library lives in namespace std, which is why you write std::cout or bring specific names in.
Avoid using namespace std; at file scope, especially in headers, because it dumps every standard name into your scope and can silently pick the wrong overload or cause ambiguities. Qualify names or pull in only what you use.
namespace geometry {
double area(double r) { return 3.14159 * r * r; }
}
double a = geometry::area(2.0);
using std::cout; // prefer targeted using-declarationsstatic has a few related meanings by context. A static local variable keeps its value between calls and is initialized once. A static member belongs to the class, not any instance, and is shared across all objects. A static function or variable at file scope has internal linkage, so it's private to that translation unit.
The unifying idea is a single, persistent instance rather than one per object or per call. Interviewers often ask you to distinguish these three uses, so The context each time.
int nextId() {
static int counter = 0; // persists across calls
return ++counter;
}
nextId(); // 1
nextId(); // 2Header files (.h or .hpp) declare interfaces: class definitions, function prototypes, templates. Source files (.cpp) hold the implementations. Other files include the header to see the interface, and the linker connects those uses to the compiled definitions.
Include guards (or #pragma once) stop a header from being included twice in one translation unit, which would cause redefinition errors. Without them, any header included transitively more than once breaks the build.
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
class Widget {
public:
void render();
};
#endif // or simply: #pragma once at the topA C++ build runs in four stages. The preprocessor expands #include, #define, and conditional directives into a single translation unit. The compiler turns that into object code and reports syntax and type errors. The assembler produces machine code, and the linker joins all object files and libraries into one executable, resolving symbols across files.
Knowing these stages explains real errors: a missing header is a preprocessor or compile error, while a missing definition of a declared function is a linker error. Those are different failures with different fixes.
From source to executable
Undefined-symbol messages come from the linker, not the compiler. That tells you a declaration exists but the definition is missing.
A macro is a preprocessor text substitution done before compilation with #define. Because it's blind text replacement, macros ignore scope and types, so a function-like macro can evaluate arguments twice or bind operators wrong without parentheses. They also don't show up in the debugger as themselves.
Modern C++ replaces most macros: const or constexpr for constants (they're typed and scoped), and inline functions or function templates for macro-like helpers (they respect types and evaluate arguments once). Reserve macros for include guards and conditional compilation, where there's no real alternative.
#define SQUARE(x) ((x) * (x)) // macro: risky, SQUARE(a+1) needs the parens
SQUARE(++i); // bug: increments i twice
constexpr int square(int x) { return x * x; } // typed, safe, one evaluation
square(5); // 25For candidates with working experience: memory ownership, the object model, templates, and the standard library judgment that separates users from understanders.
RAII, Resource Acquisition Is Initialization, ties a resource's lifetime to an object's scope. You acquire the resource in the constructor and release it in the destructor, so when the object goes out of scope the resource is freed automatically, even if an exception unwinds the stack.
It's the reason C++ can be safe without a garbage collector. Files, locks, memory, and sockets all become self-managing objects. Smart pointers, std::lock_guard, and std::vector are RAII types you already use.
#include <mutex>
std::mutex m;
void safeUpdate() {
std::lock_guard<std::mutex> lock(m); // acquires
// ... critical section ...
} // destructor releases the lock, even on exceptionKey point: RAII is the single most important idea in modern C++. If you can explain it clearly with the lock or file example, you've cleared a big bar.
Smart pointers are RAII wrappers around raw pointers that free memory automatically. std::unique_ptr is sole ownership with zero overhead: it can't be copied, only moved. std::shared_ptr allows shared ownership with a reference count and frees the object when the last owner dies. std::weak_ptr is a non-owning observer of a shared_ptr that breaks reference cycles.
Default to unique_ptr; it covers most cases and costs nothing over a raw pointer. Reach for shared_ptr only when ownership is genuinely shared, and use weak_ptr to point back without keeping the object alive.
#include <memory>
auto u = std::make_unique<int>(5); // sole owner
auto s = std::make_shared<int>(9); // ref-counted
std::weak_ptr<int> w = s; // observes, doesn't own
if (auto locked = w.lock()) { // still alive?
// use *locked
}| Smart pointer | Ownership | Copyable | Use when |
|---|---|---|---|
| unique_ptr | Single | No (movable) | One clear owner (default choice) |
| shared_ptr | Shared (ref-counted) | Yes | Ownership is genuinely shared |
| weak_ptr | None (observer) | Yes | Break cycles, cache-style references |
Key point: The follow-up is 'why does shared_ptr need weak_ptr?'. the technical answer is reference cycles: two shared_ptrs pointing at each other never reach a count of zero.
Watch a deeper explanation
Video: SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr) (The Cherno, YouTube)
A memory leak happens when you allocate with new and never delete, so the memory is never reclaimed. A dangling pointer points to memory that's already been freed, and dereferencing it is undefined behavior. Both come from manual lifetime management going wrong.
Prevent both by not managing raw memory by hand. Use smart pointers for ownership, containers for collections, and RAII for every resource. When you do hold a raw pointer, treat it as a non-owning observer and never delete through it.
// leak: no delete
void leak() { int* p = new int(5); } // p lost, memory leaked
// dangling: use after free
int* dangle() {
int local = 5;
return &local; // returns address of a dead variable
}A derived class inherits the members of its base class and can add or override behavior. Public inheritance models an is-a relationship, so a Dog is-an Animal. The derived class gets the base's public and protected members and can call the base constructor in its initializer list.
C++ also allows multiple inheritance, which brings power and the diamond problem when two bases share a common ancestor. That's solved with virtual inheritance, but many designs avoid the situation by preferring composition.
class Animal {
public:
void breathe() { /* ... */ }
};
class Dog : public Animal { // Dog is-an Animal
public:
void bark() { /* ... */ }
};
Dog d;
d.breathe(); // inherited
d.bark(); // own methodA virtual function lets a derived class override a base class method so that calling through a base pointer or reference runs the most-derived version. This is runtime polymorphism, and it's resolved through a per-class table of function pointers called the vtable.
Each polymorphic object carries a hidden pointer to its class's vtable; a virtual call looks up the right override at runtime. That indirection has a small cost, which is why not every function is virtual by default in C++.
class Shape {
public:
virtual double area() const { return 0; }
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
Circle(double radius) : r(radius) {}
double area() const override { return 3.14159 * r * r; }
};
Shape* s = new Circle(2.0);
s->area(); // calls Circle::area via the vtableKey point: Say override explicitly on derived methods. It's not required, but it makes the compiler catch signature mistakes, and interviewers notice the habit.
Watch a deeper explanation
Video: Practical Polymorphism C++ (javidx9, YouTube)
When you delete a derived object through a base class pointer, the destructor call is dispatched like any function. If the base destructor isn't virtual, only the base part is destroyed, the derived destructor never runs, and you leak whatever the derived class owned. That's undefined behavior.
The rule is simple: if a class has any virtual function, or is meant to be used through base pointers, give it a virtual destructor. Making it = default keeps it free to write.
class Base {
public:
virtual ~Base() = default; // required for safe polymorphic delete
};
class Derived : public Base {
int* buffer = new int[100];
public:
~Derived() { delete[] buffer; }
};
Base* b = new Derived();
delete b; // runs ~Derived then ~Base, thanks to virtualA pure virtual function is declared with = 0 and has no implementation in the base class. Any class with at least one pure virtual function is abstract and can't be instantiated; it exists to define an interface that derived classes must implement.
This is how C++ expresses interfaces. A class of only pure virtual functions plus a virtual destructor is the C++ equivalent of a Java interface. Derived classes become concrete once they override every pure virtual.
class Drawable {
public:
virtual void draw() const = 0; // pure virtual
virtual ~Drawable() = default;
};
// Drawable d; // error: abstract
class Button : public Drawable {
public:
void draw() const override { /* ... */ }
};Compile-time (static) polymorphism is resolved by the compiler: function overloading and templates pick the right code based on types known at compile time, with no runtime cost. Runtime (dynamic) polymorphism is resolved while the program runs, through virtual functions and the vtable.
Use static polymorphism when the types are known at compile time and you want zero overhead. Use dynamic polymorphism when you need to treat different concrete types uniformly through a common base interface at runtime.
| Compile-time | Runtime | |
|---|---|---|
| Mechanism | Overloading, templates | Virtual functions |
| Resolved | At compile time | At runtime (vtable) |
| Cost | None | Small indirection |
| Flexibility | Types fixed at compile | Types chosen at runtime |
Templates let you write code once that works for many types. The compiler generates a concrete version for each type you actually use, so you get generic code with no runtime cost. A function template parameterizes a function; a class template parameterizes a whole class, like std::vector<T>.
The standard library's containers and algorithms are templates. The trade-offs to mention are longer compile times and error messages that can get dense, though concepts in C++20 make those messages far clearer.
template <typename T>
T maxOf(T a, T b) { return a > b ? a : b; }
maxOf(3, 7); // int version generated
maxOf(2.5, 1.5); // double version generated
template <typename T>
class Box {
T value;
public:
Box(T v) : value(v) {}
T get() const { return value; }
};The Standard Template Library is the part of the C++ standard library built on templates. Its three pillars are containers (vector, map, set, list, and more), algorithms (sort, find, accumulate, transform), and iterators, which the two by giving algorithms a uniform way connects to traverse any container.
The design lets one algorithm work on any container: std::sort works on a vector or a deque because both provide the iterators it needs. Knowing which container fits which access pattern is exactly what intermediate interviews probe.
#include <vector>
#include <algorithm>
std::vector<int> v = {4, 1, 3, 2};
std::sort(v.begin(), v.end()); // algorithm + iterators
auto it = std::find(v.begin(), v.end(), 3);std::vector is a contiguous dynamic array: fast index access and iteration, cheap push_back at the end. std::list is a doubly linked list: fast insert and erase anywhere, but no random access and poor cache behavior. std::map is an ordered tree with O(log n) lookup; std::unordered_map is a hash table with average O(1) lookup but no ordering.
The default is vector, because contiguous memory is cache-friendly and wins in practice more often than big-O suggests. Use unordered_map for fast key lookup, map when you need keys sorted, and list rarely, only when you insert and erase in the middle constantly.
Lookup work by container at 1,000,000 elements
Order of comparisons or probes to find one key, N = 1,000,000 (log scale). unordered_map is average O(1), map is O(log N) = 20, a linear scan of a vector is O(N).
| Container | Lookup | Insert/erase middle | Ordered |
|---|---|---|---|
| vector | O(1) by index | O(n) | Insertion order |
| list | O(n) | O(1) at a known spot | Insertion order |
| map | O(log n) by key | O(log n) | Sorted by key |
| unordered_map | O(1) average by key | O(1) average | No |
An iterator is an object that points into a container and knows how to move to the next element, generalizing the idea of a pointer. Algorithms operate on iterator ranges (begin, end) rather than specific containers, which is what makes the STL composable.
The categories, from weakest to strongest, are input, output, forward, bidirectional, and random-access. A vector gives random-access iterators (jump anywhere in O(1)); a list gives bidirectional ones (step forward or back only). An algorithm's requirements tell you which containers it supports.
std::vector<int> v = {10, 20, 30};
for (auto it = v.begin(); it != v.end(); ++it) {
*it += 1;
}
// range-based for is the modern shorthand:
for (int& x : v) { x += 1; }A copy constructor creates a new object as a copy of an existing one. Its signature takes a const reference to the same class: MyClass(const MyClass& other). It's called when you initialize one object from another, pass an object by value, or return one by value (subject to elision).
The compiler generates a member-wise copy for you. That's fine until the class owns a raw resource like heap memory, where a shallow copy leaves two objects pointing at the same buffer, a double-free waiting to happen. That case needs a hand-written deep copy.
class Buffer {
int* data;
size_t n;
public:
Buffer(const Buffer& other) : n(other.n) {
data = new int[n];
std::copy(other.data, other.data + n, data); // deep copy
}
};Rule of Three: if a class needs a custom destructor, copy constructor, or copy assignment, it almost certainly needs all three, because it's managing a resource. Rule of Five extends that to the move constructor and move assignment once move semantics enter the picture.
Rule of Zero is the modern goal: design classes so they need none of these by holding resources in types that already manage themselves (smart pointers, containers). Then the compiler-generated special members just work, and you write less error-prone code.
// Rule of Zero: no manual special members needed
class Session {
std::string id;
std::vector<int> events;
std::unique_ptr<Connection> conn;
// copy/move/destroy all handled by members
};Key point: Landing on Rule of Zero as the goal, not just reciting Three and Five, is what signals modern C++ maturity.
Operator overloading lets you define what operators like +, ==, or << mean for your own types, so a Vector2 can support v1 + v2 or a Money type can compare with ==. You implement them as member or free functions with special names like operator+.
Use it only when the operator's meaning is obvious for your type, math types, string-like types, comparisons. Overloading operators to do something surprising hurts readability, so the test is whether a reader would guess the behavior correctly.
struct Vec2 {
double x, y;
Vec2 operator+(const Vec2& o) const {
return {x + o.x, y + o.y};
}
bool operator==(const Vec2& o) const {
return x == o.x && y == o.y;
}
};You throw an exception with throw, catch it with try/catch, and the runtime unwinds the stack from the throw point to the matching handler, running destructors along the way. That stack unwinding is why RAII makes exceptions safe: resources release automatically as scopes exit.
Catch by const reference to avoid slicing and copying (catch (const std::exception& e)), throw by value, and derive custom exceptions from std::exception. Destructors should not throw, because an exception escaping a destructor during unwinding terminates the program.
#include <stdexcept>
double divide(int a, int b) {
if (b == 0) throw std::invalid_argument("divide by zero");
return double(a) / b;
}
try {
divide(10, 0);
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}auto tells the compiler to deduce a variable's type from its initializer at compile time. It's not dynamic typing; the type is fixed and known, you just don't spell it out. It's most useful for verbose types like iterators and for making generic code read cleanly.
Use auto to cut noise (auto it = m.begin() instead of the full iterator type) and to avoid accidental conversions. Be explicit when the exact type matters for readability, and remember auto strips references and const unless you write auto& or const auto&.
std::map<std::string, int> scores;
for (const auto& [name, score] : scores) { // structured bindings
std::cout << name << ": " << score << '\n';
}A lambda is an anonymous function object you can define inline, useful for short callbacks passed to algorithms. The capture list in square brackets controls which surrounding variables the lambda can use: [=] captures by value (a copy), [&] captures by reference, or you name specific variables.
Capture by reference only when the lambda won't outlive the captured variables, or you get a dangling reference. Lambdas that capture nothing convert to plain function pointers; those that capture become unnamed function objects the compiler generates.
int threshold = 10;
std::vector<int> v = {5, 12, 8, 20};
auto count = std::count_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; }); // captures by value
// count == 2const means a value won't change after initialization, but that value can be computed at runtime. constexpr means the value can be computed at compile time and can be used where a compile-time constant is required, like an array size or template argument.
Every constexpr is implicitly const, but not every const is constexpr. Prefer constexpr for true compile-time constants and functions you want evaluated during compilation; it moves work out of runtime entirely.
const int a = readFromFile(); // runtime value, then fixed
constexpr int b = 4 * 8; // computed at compile time
constexpr int square(int n) { return n * n; }
int arr[square(3)]; // fine: 9 known at compile timeadvanced rounds probe internals, ownership design, undefined behavior, and production scars. Expect every answer here to draw a follow-up.
Move semantics let you transfer ownership of a resource instead of copying it. When an object is about to be destroyed anyway, like a temporary, its guts (a heap buffer, a file handle) can be stolen rather than duplicated. A move constructor and move assignment take an rvalue reference and leave the source in a valid but empty state.
The payoff is performance: returning a large vector or passing one through layers no longer copies megabytes. std::move casts to an rvalue reference to opt into moving. Combined with copy elision, this is why modern C++ returns big objects by value without guilt.
class Buffer {
int* data; size_t n;
public:
Buffer(Buffer&& other) noexcept // move constructor
: data(other.data), n(other.n) {
other.data = nullptr; // steal, then null the source
other.n = 0;
}
};
std::vector<int> big = makeBig();
std::vector<int> v = std::move(big); // no copy, big is now emptyKey point: Mark move operations noexcept. Containers like vector only use your move (instead of copy) during reallocation if it can't throw. That detail signals real depth.
Watch a deeper explanation
Video: Move Semantics in C++ (The Cherno, YouTube)
An lvalue is an expression that names a persistent object with an address you can take, like a variable. An rvalue is a temporary or literal that doesn't have a stable identity, like the result of a + b or the number 5. The distinction drives which overloads and reference bindings apply.
An rvalue reference (T&&) binds to rvalues and is the hook for move semantics: it lets you detect 'this thing is about to disappear, so I can steal from it.' Named rvalue references are themselves lvalues, which is why you still write std::move inside a move constructor.
int x = 10;
int& lref = x; // binds to lvalue
int&& rref = 20; // binds to rvalue (temporary)
// int&& bad = x; // error: x is an lvalue
int&& ok = std::move(x); // std::move produces an rvaluePerfect forwarding passes arguments through a template function to another function while preserving their value category (lvalue vs rvalue) and const-ness. It's what lets wrappers like std::make_unique forward constructor arguments without extra copies or losing move-ability.
A template parameter written as T&& is a forwarding (universal) reference: it binds to both lvalues and rvalues, and reference collapsing decides the actual type. You forward it with std::forward<T>(arg), which casts back to the original category. Use std::forward, not std::move, in this position.
template <typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// lvalue args stay lvalues, rvalue args stay rvalues, no needless copiesKey point: The trap is using std::move where std::forward belongs. Explaining reference collapsing (T&& && becomes T&&) is what separates memorized from understood here.
Undefined behavior means the standard places no requirements on what happens: the program might crash, produce garbage, or appear to work until it doesn't. The compiler is allowed to assume UB never happens, which is why it can silently delete code or produce baffling results.
Common causes: dereferencing a null or dangling pointer, reading uninitialized memory, out-of-bounds array access, signed integer overflow, use-after-free, and violating the one-definition rule. The senior habit is compiling with warnings, sanitizers (ASan, UBSan), and treating UB as a bug to eliminate, not a quirk to route around.
For each polymorphic class the compiler builds a vtable: an array of pointers to that class's virtual function implementations. Every object of the class stores a hidden vptr pointing at its class's vtable. A virtual call reads the vptr, indexes into the vtable, and jumps, so it's one extra indirection over a normal call.
The costs are one pointer per object (the vptr) and the missed inlining plus the indirect jump per call. For hot inner loops this matters, which is why C++ makes you opt into virtual rather than paying for it everywhere. Devirtualization can remove the cost when the compiler proves the concrete type.
Object slicing happens when you copy a derived object into a base object by value: only the base part is copied, and the derived data and behavior are sliced off. The result is a plain base object, and virtual calls on it no longer reach the derived override.
Avoid it by handling polymorphic objects through base pointers or references, never by value. Storing a std::vector<Base> slices; a std::vector<std::unique_ptr<Base>> preserves the full objects. Deleting the copy operations on a polymorphic base also makes accidental slicing a compile error.
Derived d;
Base b = d; // SLICED: only the Base part copied
Base& r = d; // fine: refers to the whole Derived
Base* p = &d; // fine: virtual calls reach DerivedSince C++11 the standard defines a memory model that says when writes by one thread become visible to another and what counts as a data race. A data race, two threads accessing the same memory with at least one write and no synchronization, is undefined behavior. std::atomic and mutexes provide the synchronization that makes access well-defined.
std::atomic operations take a memory order (seq_cst by default, down to relaxed) that controls how much reordering the compiler and CPU may do around them. Most code should stick with the default sequential consistency; relaxed and acquire/release orderings are for measured, well-understood hot paths.
#include <atomic>
#include <thread>
std::atomic<int> counter{0};
void work() {
for (int i = 0; i < 1000; ++i)
counter.fetch_add(1, std::memory_order_relaxed);
}
// no data race: atomic increments are synchronizedKey point: The follow-up is often 'when is relaxed ordering safe?'. A counter you only read after all threads join is the clean example.
Template metaprogramming uses templates to compute types and values at compile time: think type traits, tag dispatch, and SFINAE to enable or disable overloads based on properties of types. It made generic libraries possible but produced dense code and brutal error messages.
Modern C++ replaced much of it with clearer tools: constexpr functions run ordinary-looking code at compile time, if constexpr branches on compile-time conditions inside one function, and C++20 concepts state template requirements directly with readable errors. Knowing both the old machinery and the modern replacements is The production-ready answer.
template <typename T>
constexpr auto absValue(T x) {
if constexpr (std::is_unsigned_v<T>)
return x; // unsigned: already non-negative
else
return x < 0 ? -x : x;
}RAII frees resources deterministically at scope exit, so you know exactly when a file closes or a lock releases, and it handles any resource, not just memory. Garbage collection reclaims memory automatically at unpredictable times and doesn't help with non-memory resources like sockets or handles.
RAII's cost is that you design ownership explicitly and can still create cycles that leak with shared_ptr. GC's cost is pause times, higher memory overhead, and no help for non-memory cleanup. C++ chooses determinism and control; that fit is why it dominates latency-sensitive systems.
Pimpl (pointer to implementation) hides a class's private members behind a pointer to a forward-declared implementation struct defined in the .cpp. The header exposes only the public interface and an opaque pointer, so changing private members doesn't force every including file to recompile.
It buys compile-time isolation and a stable ABI for libraries, at the cost of a heap allocation and an extra indirection per access. Worth it for widely-included headers and library boundaries; overkill for internal classes where the recompile cost is small.
// widget.h
class Widget {
public:
Widget();
~Widget();
void render();
private:
struct Impl; // forward declared
std::unique_ptr<Impl> impl; // hidden details
};C++11 was the big modernization: auto, range-based for, lambdas, smart pointers, move semantics, rvalue references, nullptr, and the threading library. C++14 refined it with generic lambdas and return-type deduction. C++17 added structured bindings, if constexpr, std::optional, std::variant, and std::string_view. C++20 brought concepts, ranges, coroutines, and modules.
For interviews, the through-line matters more than the checklist: each standard pushed C++ toward safer defaults and clearer generic code. Naming what you actually use, and why, reads better than reciting the full list.
| Standard | Headline features |
|---|---|
| C++11 | auto, lambdas, smart pointers, move semantics, threads |
| C++14 | Generic lambdas, return type deduction |
| C++17 | Structured bindings, if constexpr, optional, variant, string_view |
| C++20 | Concepts, ranges, coroutines, modules |
Copy elision lets the compiler skip copy or move constructors and build the object directly in its destination. Return value optimization (RVO) is the common case: a function returning a local object constructs it straight into the caller's storage, so no copy or move happens at all.
Since C++17, elision of a returned temporary is guaranteed, not just permitted. The practical rule: return by value freely; don't write return std::move(local), because that actually blocks RVO by turning a named object into an rvalue expression the compiler can't elide the same way.
std::vector<int> build() {
std::vector<int> v(1000, 0);
return v; // RVO: no copy, no move
// return std::move(v); // don't: this can prevent RVO
}static_cast does compile-time-checked conversions between related types (numeric conversions, up/down the hierarchy without runtime check). dynamic_cast does a runtime-checked downcast in a polymorphic hierarchy, returning nullptr (or throwing for references) on failure. const_cast adds or removes const. reinterpret_cast reinterprets the bit pattern, the sharpest and least safe tool.
Prefer static_cast for ordinary conversions and dynamic_cast when you genuinely need to test a runtime type. Reach for const_cast and reinterpret_cast rarely and with a comment, because they're where UB tends to hide. C-style casts are discouraged precisely because they silently pick whichever of these applies.
| Cast | Checked | Use for |
|---|---|---|
| static_cast | Compile time | Numeric and related-type conversions |
| dynamic_cast | Runtime | Safe downcast in polymorphic hierarchies |
| const_cast | None | Add or remove const |
| reinterpret_cast | None | Bit-level reinterpretation (rare) |
Reproduce or observe first: capture the core dump and open it in a debugger for the stack at the crash, and pull logs around the failure. If it's intermittent memory corruption, rebuild with AddressSanitizer and UBSan and run the workload, because they catch use-after-free, buffer overflows, and UB that a normal build hides.
Then narrow: valgrind or ASan for the exact bad access, correlate with recent changes, and check the usual suspects, dangling references, ownership handed to two owners, iterator invalidation after a container mutation, and threading races. Fix at the right layer and confirm with the sanitizer clean. The method matters more than any single tool: observe, isolate, hypothesize, verify.
Key point: Naming ASan/UBSan and iterator invalidation shows you've debugged real C++, not just read about it.
Clarify capacity and the required operations first, then reach for the standard pattern: a doubly linked list to track recency order plus a hash map from key to the list node. get moves the node to the front and returns its value; put inserts at the front and evicts the back node when over capacity. Both operations are O(1).
In C++ that's std::list for the order and std::unordered_map<Key, std::list<...>::iterator> for the lookup, since list iterators stay valid across other insertions and erasures. The closing step is the edges: thread safety (wrap with a mutex if shared), and whether eviction needs a callback. Structuring the answer, clarify, data structures, complexity, edges, is what's really being evaluated.
#include <list>
#include <unordered_map>
class LRUCache {
size_t cap;
std::list<std::pair<int,int>> items; // front = most recent
std::unordered_map<int, std::list<std::pair<int,int>>::iterator> index;
public:
LRUCache(size_t c) : cap(c) {}
int get(int key) {
auto it = index.find(key);
if (it == index.end()) return -1;
items.splice(items.begin(), items, it->second); // move to front
return it->second->second;
}
void put(int key, int value) {
auto it = index.find(key);
if (it != index.end()) {
it->second->second = value;
items.splice(items.begin(), items, it->second);
return;
}
if (items.size() == cap) {
index.erase(items.back().first);
items.pop_back();
}
items.emplace_front(key, value);
index[key] = items.begin();
}
};C++ wins when you need C-level control over memory and layout plus higher-level abstractions like classes and templates, without a garbage collector pausing your code. It trades safety and simplicity for that control: manual ownership and undefined behavior are real costs, which is exactly why Rust exists and why Java chose a managed runtime. Knowing these trade-offs out loud is itself an interview signal, because it shows you pick tools on merits rather than habit.
| Language | Memory model | Best at | Watch out for |
|---|---|---|---|
| C++ | Manual, RAII, smart pointers | Games, systems, low-latency services | Undefined behavior, ownership bugs |
| C | Fully manual | Kernels, embedded, tiny footprints | No classes, no RAII, more boilerplate |
| Rust | Ownership checked at compile time | Systems code with memory safety | Steeper learning curve, borrow checker |
| Java | Garbage collected | Enterprise apps, cross-platform services | GC pauses, less control over layout |
Prepare in layers, and practice out loud. Most C++ rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical C++ interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These C++ questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works