LearnThatStack Ace your next interview

C++

Assumes C++17. C++20 and later rows name their version Checked Nov 2026 90 code snippets

The model

  • RAII ties a resource to an object. The destructor releases it on every exit from the scope, even a thrown exception.
  • The type says who owns memory. unique_ptr has one owner, and shared_ptr has several. weak_ptr and T& own nothing.
  • A copy duplicates the data. A move takes over the source's resources, so it costs O(1). std::move itself is only a cast.
  • Default to the stack and std::vector. Use the heap when an object must outlive its scope or is too big for a frame.
  • Undefined behaviour, such as reading freed memory, often looks fine in a test. So run tests under the sanitizers.
Scopea { } blockOwnerobject on the stackNew ownerafter a moveResourceheap, file, lockBUILDenterconstructor acquiresMOVEmove hands overFREEexit return or throwdestructor releases
The constructor acquires the resource and the destructor releases it, on return or on throw. After a move, the new owner is the one that frees it.

Stack, heap & pointers

where objects live and how you reach them

Stack, heap, new and malloc

Make a local that frees itselfT t;
Use the heapWhen the object must outlive its scope, is too big for a frame, or has a runtime size.
Say why the stack allocates fasterIt moves a pointer. The heap allocator must find a block.
Build and destroy one objectT* p = new T;delete p;
Get raw bytes with no object in themvoid* m = malloc(n);free(m);

TipThe stack is small, often 1 to 8 MB. Deep recursion or a huge local array overflows it.

TrapMixing the pairs is undefined behaviour, and new[] pairs only with delete[].

Smart pointers

say in the type who owns a heap object

Pick a smart pointer

Use one owner, the defaultauto u = std::make_unique<T>();
Share when no single owner fitsauto s = std::make_shared<T>();
Watch a shared object without owning itstd::weak_ptr<T> w = s;
Name the usual causes of a leakA new with no delete, or a return or throw that skips cleanup.
Find where a leak came fromvalgrind --leak-check=full ./app

TrapAn owning T* looks the same as a borrowed one, so the compiler can't catch a leak or a double delete.

Pass a unique_ptr

Give a function ownershipvoid f(std::unique_ptr<T> p);f(std::move(u));
Let a function only read the objectvoid f(const T& t);

TrapTaking unique_ptr& says the function may or may not take ownership. That is the ambiguity smart pointers exist to remove.

How shared_ptr counts owners

ctrl blockowner 1owner 2object
two pointersEach owner holds two pointers. make_shared puts both targets in one allocation.
Say when the object diesA copy raises the strong count in the control block. At zero, the object dies.
Know the drawback of make_sharedThe memory stays until the last weak_ptr dies.

TrapTwo shared_ptr built from one raw pointer make two control blocks, so the object is deleted twice.

Lifetime, RAII & copies

when objects die and what the compiler writes for you

RAII and destruction order

Close a file when the scope endsstd::ifstream f{path};
Say why it beats a cleanup callReturns and exceptions skip a cleanup call. Destructors always run.
Give the order for localsReverse of construction. The newest local dies first.
Give the order inside one objectDestructor body, then members in reverse order, then bases.
Say what a throwing constructor cleans upEach fully built member. The object's own destructor never runs, so a raw new member leaks.

TrapOnly automatic storage, meaning ordinary locals, gets this. Scope exit destroys the pointer to a new object, not the object.

Rule of Three, Five and Zero

Aim for the Rule of ZeroHold vector and unique_ptr members, and declare none of the five.
Apply the Rule of ThreeA custom destructor also needs a copy constructor and copy assignment.
Apply the Rule of FiveAdd the move constructor and move assignment.
Keep the move when you add a destructorT(T&&) = default;The compiler writes one only if you declared none of the other four.

TrapA destructor added for one log line stops the implicit move. Every move then silently copies.

Move semantics

hand resources over instead of copying them

What a move does

Say what a move savesA vector move copies a few pointers. A copy copies every element.
Say what std::move doesIt casts to an rvalue reference. It moves no bytes.
Declare a move constructorT(T&& o) noexcept;Copy the handles, then blank the source.
Say when overload resolution picks the moveFor a temporary or a std::move cast. A named object copies.

Trapstd::move on a const object, or a type with no moves, falls back to a full copy.

std::move or std::forward

Hand over a local or a concrete T&&std::move(x)
Pass on a deduced T&&std::forward<T>(x)
Spot a forwarding referenceA T&& deduced on that same function. An lvalue makes it collapse to U&.

TipA forwarding constructor beats the copy constructor for non-const lvalues, so constrain it.

Trapstd::move on a deduced T&& silently empties a variable the caller still owns.

Moved-from objects and returns

Use a moved-from objectDestroy it or assign a new value. Don't assume it is empty.
Name the types left nullunique_ptr and shared_ptr. Others are valid but unspecified.
Return a localreturn w;
Move out of a memberreturn std::move(p.first);

TipShort strings live inside the object, so a moved-from std::string may keep its characters.

Trapreturn std::move(w); disables elision and usually makes the code slower.

Exception safety in moves and copies

Buf& operator=(Buf rhs) { // the copy may throw here swap(*this, rhs); // your noexcept swap return *this; // rhs frees the old state}// std::swap would call operator= and recurse
Promise a move won't throwT(T&&) noexcept;If it throws anyway, the program calls std::terminate.

TipCopy-and-swap gives the strong guarantee, but it always allocates. For big buffers in a loop, hand-write the reuse path.

TrapWithout noexcept, a copyable type is copied, not moved, when a vector grows.

Virtual functions & classes

run the derived version through a base pointer

Virtual functions and slicing

Offer a default that a derived class may replacevirtual void draw();
Replace it in a derived classvoid draw() override;
Force derived classes to supply onevirtual double area() const = 0;The class is then abstract.
Pass a polymorphic objectvoid f(const Base& b);
Store polymorphic objectsvector<unique_ptr<Base>> v;

TipWithout virtual, the static type decides, and your override is skipped.

TrapCopying a derived object into a base variable keeps only the base part. By-value parameters and vector<Base> do this.

How a virtual call works

object 1object 2vtablearea()draw()
dispatchEach object's vptr points to one shared vtable. A fixed slot holds each function.
Count the vtables for five objectsOne per class. Five objects just hold five vptrs.
Say when the vtable is createdAt compile time. It is static data in the binary.

TipThe cost that shows in profiles is lost inlining. On a final class, the compiler can devirtualize the call.

Virtual destructors and constructors

Allow delete through a base pointervirtual ~Base() = default;
Refuse delete through a base pointerprotected: ~Base() = default;
Make a class abstract through its destructorvirtual ~Base() = 0;Base::~Base() {}Leave out the body and you get a link error.
Copy the real type behind a base referencevirtual std::unique_ptr<Base> clone() const = 0;

TipA constructor can't be virtual, because it is what installs the vptr. Inside one, a virtual call runs the class being built.

TrapDeleting through a base with no virtual destructor is undefined behaviour. The derived destructor usually never runs.

The diamond problem

AABCD
plainD holds two A objects, so d.value is ambiguous.
ABCD
virtualB and C inherit virtual A, so D holds one shared A.
Share one basestruct B : virtual A {};

TrapThe most derived class initializes the virtual base, so D constructs A itself.

Containers & iterators

pick the container, keep your iterators valid

Pick a standard container

Store a list of things, the defaultstd::vector<T>
Push and pop at both endsstd::deque<T>
Splice ranges, or keep iterators valid through insertsstd::list<T>
Keep keys sorted, O(log n) lookupstd::map<K, V>
Hash keys, average O(1) lookupstd::unordered_map<K, V>
Narrow another container's interfacestack, queue and priority_queue are adaptors.
Read a key, and throw or insert a default if missingm.at(k)m[k]
Test for a key (C++20)m.contains(k)

TipThe four families are sequence, ordered, unordered and adaptors. How they store elements decides what is cheap.

TrapA list insert still costs O(n) to reach the position. A hash table can lose on long string keys.

C array, vector and capacity

Use a fixed block that stores no sizeint a[3];
Use a fixed size with a real typestd::array<int, 3> a;
Own a buffer that grows and knows its sizestd::vector<int> v;
Read the size and the capacityv.size()v.capacity()
Avoid repeated reallocationv.reserve(n);

TipGrowth is geometric, so push_back is amortised O(1). Capacity never shrinks on its own.

TrapA C array decays to a pointer when you pass it. Indexing past its end is undefined behaviour with no diagnostic.

Iterators and invalidation

Change a vectorGrowth invalidates all. erase invalidates from that position on.
Insert into a dequeInvalidates all iterators.
Erase from a list, map or setInvalidates only the erased element.
Rehash an unordered_mapInvalidates iterators. Pointers and references stay valid.
Erase inside a loopit = c.erase(it);
List the iterator categoriesInput and output, forward, bidirectional, random access, contiguous.

Tipstd::sort needs random access, so it won't compile on a list.

TrapUsing an invalidated iterator is undefined behaviour, though it usually appears to work.

Templates & constexpr

generate code and compute at compile time

Templates and specialization

Deduce the type from the calltemplate <class T> T twice(T x);
Deduce a class template's type (C++17)std::vector v{1, 2};
Say what the compiler generatesA separate instantiation for each type. It adds no runtime cost.
Give one type its own versiontemplate <> struct Box<bool> {};
Give every pointer type a versiontemplate <class T> struct Box<T*> {};

TipFunction templates can't be partially specialized. A plain overload is usually the clearer fix.

TrapThe definition must be visible where it is instantiated. That is why template code lives in headers.

Concepts and SFINAE

Require a property (C++20)template <std::integral T>
Constrain one parameter (C++20)void f(std::integral auto x);
Constrain a template before C++20Use enable_if. A bad substitution removes only that candidate.
Choose SFINAE over specializationTo test a property, like having begin(), not one exact type.

TrapSFINAE only recovers failures in the signature. An invalid expression in the function body is a hard error.

const, constexpr and mutable

Forbid writes through this nameconst int n = read();
Require a compile-time valueconstexpr int n = 16;
Force compile-time evaluation (C++20)consteval int f();
Change a member inside a const functionmutable int hits_ = 0;

Tipmutable fits a cache or a counter, but guard it if several threads read the object.

TrapA constexpr function runs at compile time only when the result must be a constant. Otherwise it is an ordinary call.

Compile-time polymorphism with CRTP

Pass the class to its own base templatestruct Square : Shape<Square> {};
Call the derived class from the basestatic_cast<const D*>(this)->areaImpl();
Skip the template base (C++23)void f(this auto&& self);

TipThere is no vtable, so the compiler can inline the call.

TrapShape<Square> and Shape<Circle> are unrelated types, so they can't share one container.

Exceptions & errors

report failure and clean up on the way out

throw, catch and your own types

Signal a failurethrow std::runtime_error("bad");
Catch by const referencecatch (const std::exception& e)
Define your own exceptionclass E : public std::runtime_error
Say what happens between throw and catchThe stack unwinds, and each fully built local is destroyed in reverse order.

TipA type outside the std::exception tree usually ends in std::terminate. So does a destructor that throws during unwinding.

TrapThe first matching handler wins, so a base-class handler listed first hides the derived handlers after it.

Lambdas, threads & strings

capture, share and view data safely

Lambda captures

Copy a variable when you create the lambda[limit](int x) { return x < limit; }
Refer to a variable and see its changes[&limit](int x) { return x < limit; }
Move a move-only value in[p = std::move(ptr)]() {}

TrapA reference capture doesn't extend the variable's lifetime. A callback that outlives the scope should capture by value.

atomic or mutex

Share a counter with no lockstd::atomic<int> hits{0};hits.fetch_add(1);
Guard data that spans several variablesstd::unique_lock lk{m};
Wait until a condition holdscv.wait(lk, [&] { return ready; });

TipTwo threads on a plain variable with no lock, and at least one writing, is a data race and undefined behaviour.

TrapAnother thread can see two atomics half-updated. An invariant across several variables still needs a mutex.

std::string_view

Read text from any source (C++17)void f(std::string_view s);
Spot a view that dangles at oncestd::string_view v = std::string("x");

TrapA view isn't null-terminated in general, so don't pass data() to a C function that expects a terminated string.

Compile, link & debug

turn source files into a program, and catch what goes wrong

Compile and link, stage by stage

Expand includes and macrosg++ -E a.cpp
Compile one unit to assemblyg++ -S a.cpp
Make an object fileg++ -c a.cpp
Declare a name, then define itint add(int, int);int add(int a, int b) { return a + b; }

TipA non-inline definition in a header that two .cpp files include gives duplicate symbol errors.

TrapAn undefined reference is a linker error. The name was declared, but a .cpp file or library was left out.

Undefined behaviour

List the memory causesUninitialized reads, indexing past the end, freed blocks, expired locals.
List the arithmetic and type causesSigned overflow, too large a shift, dividing by zero, a bad downcast.
Read an uninitialized local (C++26)It is erroneous behaviour now, not undefined behaviour.
Catch it in testsg++ -g -fsanitize=address,undefined a.cpp

TipCompilers optimize as if undefined behaviour never happens. So the crash lands far from the mistake.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 65 questions it links, not just the 8 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.