C++ Interview Questions
15 C++ questions — references vs pointers, RAII, smart pointers, the rule of five, virtual functions and the vtable, and move semantics. The memory-model depth systems and quant interviewers dig into. Tagged for FAANG and startups.
| Reference | Pointer | |
|---|---|---|
| Null | Cannot be null | Can be null |
| Reassign | Bound once at init | Can point elsewhere |
| Init required | Yes | No |
| Arithmetic | No | Yes (p+1) |
| Syntax | Acts like the object | Needs * / -> |
- By value — copies the argument; caller unaffected. Fine for small types (
int,double). - By reference (
T&) — no copy; the function can modify the caller's object. - By const reference (
const T&) — no copy, cannot modify. The default for large objects.
void print(const std::string& s); // no copy, read-only void sort(std::vector<int>& v); // mutates caller's vector
const correctness mean?C++▾const everywhere the data should not change — parameters, member functions, and pointers. It documents intent, lets the compiler catch accidental mutation, and enables optimizations.
int size() const; // method doesn't modify the object const int* p; // pointer to const int (can't change *p) int* const q = &x; // const pointer (can't repoint q)
const int* p = "p is a pointer to a const int"; int* const q = "q is a const pointer to an int".struct vs class in C++?C++▾public by default in a struct, and private by default in a class. Both can have constructors, methods, access specifiers, and inheritance.
struct for passive data aggregates (plain fields), class for types with invariants and behaviour. It's a readability signal, not a technical one.- Compile-time (static) — function/operator overloading and templates. Resolved by the compiler; zero runtime cost.
- Runtime (dynamic) —
virtualfunctions dispatched through the vtable, based on the object's actual type at run time.
Overloading is two functions in the same scope with the same name and different parameter lists; the compiler picks one at the call site. Overriding replaces a virtual base function in a derived class with the same signature, resolved through the vtable at runtime. Hiding is the accident: a derived member with the same name but a different signature hides every base overload of that name.
using Base::f; in the derived class brings the base overloads back into scope, and override makes the compiler reject a mismatched signature outright.explicit do on a constructor?C++▾It blocks implicit conversion. A single-argument constructor without explicit doubles as a conversion operator, so the compiler will silently build your type to make a call work.
struct Buffer {
Buffer(int size); // implicit
explicit Buffer(int size); // opt out
};
void send(Buffer b);
send(42); // compiles with the first; rejected with the second
Mark single-argument constructors explicit by default. Leave it off only when the conversion is genuinely something you want at every call site, such as std::string from a string literal.
An lvalue has an identity you can take the address of: a named variable, a dereferenced pointer, a function returning T&. An rvalue is a temporary with no name: a literal, an arithmetic result, a function returning by value.
int x = 5; // x is an lvalue, 5 is an rvalue int& a = x; // fine: lvalue reference binds to an lvalue int& b = 5; // error int&& c = 5; // fine: rvalue reference binds to a temporary const int& d = 5; // also fine: const lvalue ref extends the temporary
The distinction is what move semantics is built on: if the compiler can prove an argument is an rvalue, its contents can be stolen rather than copied.
static has three different meanings in C++. Name them.C++▾At file scope it gives a variable or function internal linkage, so it is private to that translation unit. Inside a function it makes a local persist across calls, initialised once on first use and destroyed at exit. Inside a class it makes a member belong to the class rather than any object, shared by every instance and callable without one.
#pragma once change?C++▾#include is textual: without a guard, a header pulled in twice pastes its declarations twice, and redefinition is an error. A guard makes the second inclusion expand to nothing.
#ifndef WIDGET_H #define WIDGET_H // ... declarations ... #endif // or, one line: #pragma once
#pragma once does the same job keyed on the file identity rather than a macro name, so it cannot collide and cannot be misspelled. It is not in the standard, but every mainstream compiler supports it.
| Stack | Heap | |
|---|---|---|
| Allocation | Automatic (scope) | Manual (new) or smart ptr |
| Speed | Very fast (pointer bump) | Slower (allocator) |
| Size | Small, fixed | Large |
| Lifetime | Ends at scope exit | Until freed |
| Risk | Overflow (deep recursion) | Leaks, fragmentation |
{
std::lock_guard<std::mutex> lk(m); // locks here
// ... critical section ...
} // unlocks automatically at scope endunique_ptr, shared_ptr, and weak_ptr.C++▾unique_ptr— sole ownership, no overhead, move-only. The default choice.shared_ptr— shared ownership via an atomic reference count; freed when the count hits zero.weak_ptr— a non-owning observer of ashared_ptr; used to break reference cycles.
auto u = std::make_unique<Widget>(); // one owner auto s = std::make_shared<Widget>(); // ref-counted std::weak_ptr<Widget> w = s; // observes, doesn't own
shared_ptrs pointing at each other never reach count zero — a leak. Make one direction a weak_ptr.int* p = new int(5); delete p; *p = 10; // UB — p dangles
nullptr, never returning addresses of locals, and using smart pointers so ownership and lifetime are explicit.
new/delete vs malloc/free?C++▾| new / delete | malloc / free | |
|---|---|---|
| Constructors | Called | Not called (raw bytes) |
| Returns | Typed pointer | void*, needs cast |
| On failure | Throws bad_alloc | Returns NULL |
| Size | Computed automatically | You compute bytes |
new/delete, and never mix the two families (don't free a new'd pointer).
Without one, delete on a Base* runs only ~Base. The derived part of the object is never destroyed, so anything it owns leaks — and the standard calls this undefined behaviour, not merely a leak.
struct Base { ~Base(); }; // not virtual
struct Derived : Base { std::vector<int> data; };
Base* p = new Derived;
delete p; // ~Derived never runs; data leaks
shared_ptr cycle, and how does weak_ptr break it?C++▾Two objects holding shared_ptrs to each other keep each other’s reference count at one forever. Neither is ever destroyed, and reference counting cannot detect it — there is no garbage collector to notice the island.
struct Node {
std::shared_ptr<Node> next; // parent -> child: ownership
std::weak_ptr<Node> prev; // child -> parent: observation
};
Make one direction non-owning. A weak_ptr does not raise the count; you call lock() to get a shared_ptr when you need the object, and get an empty one if it is already gone. Parent-child and observer links are the usual places this shows up.
Reproduce under a tool rather than reading code. AddressSanitizer (-fsanitize=address) is built into Clang and GCC and reports leaks with allocation stacks at exit. Valgrind’s memcheck needs no rebuild but runs far slower. On Windows, CRT debug heap or Application Verifier.
The structural fix is almost always ownership: a raw new with no owner becomes a unique_ptr, and the leak class disappears rather than getting patched.
std::move actually do?C++▾Nothing at runtime. It is a cast: it turns its argument into an rvalue reference so that overload resolution picks the move constructor or move assignment instead of the copying one. The moving is done by that constructor, not by move itself.
std::string a = "hello"; std::string b = std::move(a); // b steals a's buffer // a is valid but unspecified: safe to assign or destroy, not to read
move costs nothing and gains nothing: moving a const object silently copies, because a move constructor cannot take a const&&. Moving a local you then return also defeats copy elision.vector::push_back amortised O(1), and what does it invalidate?C++▾A vector grows geometrically — typically doubling. Copying n elements happens only on the reallocations, and the doubling makes the total copying across n pushes O(n), so the cost per push averages out to a constant even though individual pushes can be O(n).
reserve() up front avoids both the copies and the invalidation.push_back that does not reallocate invalidates only the end iterator. Erasing invalidates from the erase point onward.
- Rule of Three: if you define any of destructor, copy constructor, or copy assignment, you probably need all three (they manage a resource).
- Rule of Five: with move semantics (C++11), add the move constructor and move assignment.
- Rule of Zero: best of all — design classes so members (smart pointers, containers) manage resources, and you define none of the five.
vector, string, and smart pointers handle resources so the compiler-generated special members just work.virtual functions gets a hidden vtable (array of function pointers). Each object stores a vptr to its class's vtable. A virtual call looks up the function through the vptr at runtime, so the derived override runs even through a base pointer.
Base* p = new Derived(); p->speak(); // Derived::speak via vtable dispatch
virtual destructor. Deleting a Derived through a Base* without one only runs ~Base, leaking the derived part.T&&) binds to temporaries, and std::move casts an lvalue to one so its resources can be stolen.
std::vector<int> a = {1, 2, 3}; std::vector<int> b = std::move(a); // steals a's buffer, no copy // a is now valid but unspecified (usually empty)
template<typename T> T maximum(T a, T b) { return a > b ? a : b; } maximum(3, 7); // stamps out int version maximum(2.5, 1.5); // stamps out double version
List<String> is just List at runtime.const vs constexpr — what is the real difference?C++▾const means "this will not be modified after initialisation" — the value may still be computed at runtime. constexpr means "this can be computed at compile time", which is a stronger claim and lets the value be used where the language demands a constant, such as an array size or a template argument.
const int a = read(); // fine: runtime value, just immutable constexpr int b = read(); // error: not a constant expression constexpr int c = 4 * 8; // fine std::array<int, c> arr; // c usable as a template argument; a is not
On a function, constexpr says it may run at compile time when its arguments allow; it still works as a normal function otherwise.
return std::move(x)?C++▾Copy elision lets the compiler construct a returned object directly in the caller’s storage, skipping the copy or move entirely. Since C++17 it is guaranteed for returning a temporary, and named return value optimisation covers most named locals too.
std::string make() {
std::string s = build();
return s; // elided: constructed in place
// return std::move(s); // forces a move, defeating elision
}
std::move turns the expression into an rvalue reference, which is no longer eligible for elision. You pay for a move you would otherwise not have made.Reading an uninitialised variable; indexing past the end of an array; dereferencing a null or dangling pointer; signed integer overflow; data race on a non-atomic; deleting through a base pointer with a non-virtual destructor.
The compiler is allowed to assume UB never happens, and optimises on that assumption. A null check after a dereference can be deleted as dead code, because the dereference proved the pointer non-null. That is why UB shows up as a wrong answer in an unrelated function rather than a crash at the fault.
-fsanitize=address,undefined in CI. Most of this class of bug is caught the first time the line executes.if constexpr (C++17) discards the untaken branch before it is instantiated, so a branch that would not compile for the current type is simply not there.
template <typename T>
void print(const T& v) {
if constexpr (std::is_pointer_v<T>) std::cout << *v; // only for pointers
else std::cout << v;
}
Before C++17 the same job needed tag dispatch or SFINAE — writing an overload whose signature is only valid for certain types via std::enable_if, so invalid candidates drop out of overload resolution instead of erroring. C++20 concepts express the same constraint readably.
[x] copies, [&x] binds a reference, [=] and [&] do either for everything used. [x = std::move(v)] initialises a capture directly (C++14), which is how you move something into a lambda.
If the lambda escapes the enclosing scope in any way, capture by value — or capture a shared_ptr so the referent stays alive as long as the lambda does.