C++ Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for freshers and experienced developers in India preparing for C++ interviews at product companies, IT services firms, fintech and trading teams, and embedded or game studios. It has 60 questions with model answers in three levels: fresher basics such as OOP, references and constructors; working-developer topics such as the rule of five, move semantics, smart pointers, the STL and threads; and senior questions on templates, concurrency, memory layout and performance. Answers follow C++17, with C++20 named where it applies, and every program output stated was checked with g++. Answer each question aloud first, then compare, and finish with the timed C++ mock test.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
- 828 open C++ jobs
What C++ interviews cover
- Core language and OOP: classes, constructors and destructors, references and pointers, const, and virtual functions with the vtable behind them.
- Resource management: RAII, the rule of zero, three and five, smart pointers and move semantics. Most C++ interviews spend the longest time here.
- The standard library: choosing between vector, map and unordered_map, iterator invalidation, and algorithms with lambdas.
- Templates and modern C++: function and class templates, C++17 features such as optional, string_view and structured bindings, and C++20 concepts for senior roles.
- For experienced roles: concurrency with threads, mutexes and atomics, memory layout, undefined behaviour, and how you found and fixed a real performance problem.
How to prepare (freshers and experienced)
- Freshers: write a small program for every topic and compile it with g++ -std=c++17 -Wall -Wextra. Predicting the output and then running it builds exactly the intuition interviewers test.
- Be able to write a small class that owns a resource by hand, then explain why the rule of zero version built on std::vector or std::unique_ptr is better.
- Practise the standard library on real tasks such as word counts, top-k scores and merging intervals, instead of writing your own linked list every time.
- Experienced developers: prepare two stories from your own work, such as a crash found with AddressSanitizer or a latency fix found with perf, with the numbers before and after.
- Know which standard your projects used (C++11, 14, 17 or 20) and which features you actually shipped, because interviewers often ask what changed between versions.
Common mistakes and interview-day tips
- Calling something 'faster' without a reason. Name the cost instead: an allocation, a copy, an indirect call or a cache miss.
- Writing new and delete in interview code. Use std::make_unique, containers and references unless the question is about manual memory.
- Forgetting the virtual destructor in a polymorphic base class, or reading an object after calling std::move on it.
- Treating undefined behaviour as a fixed answer. If code overflows a signed int or reads freed memory, say that it is undefined instead of guessing an output.
- Think aloud while coding, test with an edge case such as an empty container, and state the time complexity without being asked.
C++ interview questions: Easy questions (freshers)
20 questions1. What are the four pillars of object-oriented programming, and how does C++ support each one?
Encapsulation, abstraction, inheritance and polymorphism. Encapsulation means a class keeps its data private and exposes member functions that protect its rules, such as a BankAccount whose balance changes only through deposit() and withdraw(). Abstraction means callers use an interface without knowing the details, often through an abstract base class. Inheritance lets a class reuse and extend a base class. Polymorphism is one interface with many behaviours: at compile time through overloading and templates, and at run time through virtual functions.
2. What is the difference between a struct and a class in C++?
Only the defaults differ. Members and base classes of a struct are public by default, while in a class they are private by default. Everything else is the same: a struct can have constructors, member functions, virtual functions, templates and private sections. By convention, teams use struct for simple data holders with no rules to protect, like a Point with x and y, and class for types that keep an invariant behind a private interface. A common fresher mistake is to say a struct cannot have functions, which is true in C, not in C++.
3. How is a reference different from a pointer in C++?
A reference is another name for an existing object: it must be initialised, cannot be null in valid code, and can never be made to refer to something else. A pointer is a separate object that stores an address: it can be null, can be reassigned and supports arithmetic. In the snippet, r = b copies 20 into a, because r stays bound to a, while p = &b really moves the pointer, so the program prints 20 30. Prefer references for parameters that must exist, and pointers when 'no object' is a valid case.
#include <iostream>
int main() {
int a = 10, b = 20;
int& r = a; // r is another name for a
r = b; // copies 20 into a; r is not reseated
int* p = &a;
p = &b; // p now points to b
*p = 30; // changes b
std::cout << a << " " << b << "\n";
}4. Why should you use a constructor's member initializer list instead of assigning in the body?
The initializer list initialises members directly, while assignment in the body first default-constructs them and then overwrites them. For a std::string that is wasted work, and for some members the list is the only option: const members, reference members and members without a default constructor must be set there, or the code does not compile. One trap: members are always initialised in the order they are declared in the class, not the order written in the list, so a member that depends on another must be declared after it. GCC warns about this with -Wall (-Wreorder).
#include <string>
#include <utility>
class Employee {
const int id_;
std::string& team_;
std::string name_;
public:
Employee(int id, std::string& team, std::string name)
: id_(id), team_(team), name_(std::move(name)) {}
};5. In what order are constructors and destructors called for a derived class object?
Construction goes from the base outwards: the base class constructor runs first, then the derived class's members are constructed in declaration order, and then the derived constructor body runs. Destruction is the exact reverse: the derived destructor body, then its members, then the base destructor. So this program prints Base() Derived() | ~Derived() ~Base(). The reason is safety: a derived object is built on top of its base part, so the base must exist first and must be destroyed last, after the derived part that may still use it.
#include <iostream>
struct Base {
Base() { std::cout << "Base() "; }
~Base() { std::cout << "~Base() "; }
};
struct Derived : Base {
Derived() { std::cout << "Derived() "; }
~Derived() { std::cout << "~Derived() "; }
};
int main() {
Derived d;
std::cout << "| ";
}6. What is the difference between function overloading and function overriding?
Overloading means several functions with the same name but different parameter lists in the same scope; the compiler picks one at compile time from the argument types, such as print(int) and print(double). Overriding means a derived class provides its own version of a base class virtual function with the same signature, and the version is chosen at run time from the real type of the object. Mark overrides with the override keyword: if the signature does not match exactly, for example a missing const, the compiler reports an error instead of silently creating a new function.
7. What is a virtual function, and what happens if you forget the virtual keyword?
A virtual function lets a call through a base class reference or pointer run the version of the object's real type. Here report() receives a Shape reference, but because area() is virtual, the Square version runs and the program prints 9. Without virtual, the call is bound at compile time to Shape::area() and prints 0, a classic bug in plugin and payment-handler code. Write override in the derived class so the compiler checks that you really override something, and give every polymorphic base class a virtual destructor.
#include <iostream>
struct Shape {
virtual double area() const { return 0; }
virtual ~Shape() = default;
};
struct Square : Shape {
double side;
explicit Square(double s) : side(s) {}
double area() const override { return side * side; }
};
void report(const Shape& s) { std::cout << s.area() << "\n"; }
int main() {
Square sq(3);
report(sq); // virtual: Square::area runs, prints 9
}8. What is a pure virtual function, and what is an abstract class?
A pure virtual function is declared with = 0, for example virtual void pay(int rupees) = 0; in a PaymentMethod base class. A class with at least one pure virtual function is abstract: you cannot create objects of it, only of derived classes that override every pure virtual function. This is how C++ expresses an interface, so checkout code can take a PaymentMethod reference and work with UPI, card or wallet classes added later. Two details interviewers like: an abstract class can still have data members and normal functions, and a pure virtual function may even have a body.
9. How are new and delete different from malloc and free?
new allocates memory and runs the constructor, returns a correctly typed pointer and throws std::bad_alloc on failure; delete runs the destructor and then frees the memory. malloc returns raw bytes as a void pointer, returns a null pointer on failure and never runs a constructor, and free never runs a destructor, so they are not suitable for objects like std::string. Never mix the pairs, and use delete[] for memory from new[]. In modern C++ you rarely write either: std::vector, std::string and std::make_unique release memory automatically.
10. What are a memory leak and a dangling pointer, and how do you avoid them?
A memory leak is memory that is allocated and never released, usually because the only pointer to it was lost or an early return or exception skipped the delete; a long-running service slowly grows until it is killed. A dangling pointer points to memory that has already been freed, or to a local variable whose function has returned. Using it is undefined behaviour and may crash or quietly corrupt data. Avoid both by letting owners such as std::unique_ptr, std::vector and std::string manage memory, and test with AddressSanitizer (-fsanitize=address), which reports both problems.
11. What is RAII, and why is it so important in C++?
RAII (Resource Acquisition Is Initialization) ties a resource to an object's lifetime: the constructor acquires it and the destructor releases it. Because C++ always runs the destructors of local objects when a scope ends, the resource is released on a normal return, an early return or an exception. In the snippet process() throws, yet the program prints open, then close, then caught: bad record, because the stack unwinds before the handler runs. The standard library uses RAII everywhere: std::fstream for files, std::lock_guard for mutexes and std::unique_ptr for memory.
#include <iostream>
#include <stdexcept>
class Connection {
public:
Connection() { std::cout << "open\n"; }
~Connection() { std::cout << "close\n"; }
};
void process() {
Connection c;
throw std::runtime_error("bad record");
}
int main() {
try {
process();
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
}12. What are unique_ptr, shared_ptr and weak_ptr, and when do you use each?
std::unique_ptr is the default: one owner, no reference count, as cheap as a raw pointer, and it can be moved but not copied. std::shared_ptr is for real shared ownership: each copy increases a reference count, and the object is deleted when the last owner goes away. std::weak_ptr observes a shared object without owning it, which breaks reference cycles and lets a cache check whether the object still exists. The snippet prints 1 42 2: u is null after the move, u2 owns 42, and s1 and s2 are the two owners, because w does not count.
#include <iostream>
#include <memory>
int main() {
auto u = std::make_unique<int>(42);
// auto u2 = u; // error: unique_ptr is move-only
auto u2 = std::move(u); // ownership moves; u is now null
auto s1 = std::make_shared<int>(7);
auto s2 = s1; // shared ownership
std::weak_ptr<int> w = s1; // observes, does not own
std::cout << (u == nullptr) << " " << *u2 << " "
<< s1.use_count() << "\n";
}13. When would you use std::vector, std::array or a plain C array?
Use std::vector when the size is decided at run time or changes. It stores elements contiguously on the heap and grows as needed, so it is the default container in most code. Use std::array when the size is fixed and known at compile time, such as 12 monthly totals: it needs no heap allocation, knows its size, and can be copied and compared. Plain C arrays are mostly for old code and C interfaces, because they decay to a pointer when passed to a function and lose their size. All three give fast index access and cache-friendly iteration.
14. What is the difference between size() and capacity() of a std::vector?
size() is the number of elements stored; capacity() is how many fit before the vector must allocate a bigger block. When push_back exceeds the capacity, the vector allocates more memory (GCC doubles it), moves the elements across and frees the old block, which also invalidates pointers and iterators into it. If you know the count, call reserve() once to avoid repeated reallocations. The snippet prints 10 1 and then 0 1: clear() removes the elements but keeps the memory. To give memory back, call shrink_to_fit(), which is only a request, or swap with an empty vector.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.reserve(100); // one allocation up front
for (int i = 0; i < 10; ++i)
v.push_back(i);
std::cout << v.size() << " " << (v.capacity() >= 100)
<< "\n";
v.clear(); // size 0, memory kept
std::cout << v.size() << " " << (v.capacity() >= 100)
<< "\n";
}15. What is the difference between std::map and std::unordered_map?
std::map is a balanced binary search tree (a red-black tree in the major implementations), so keys stay sorted and lookup, insert and erase are O(log n). std::unordered_map is a hash table: lookups are O(1) on average but can degrade to O(n) with many collisions, and the iteration order is not defined. A map needs only operator< on the key, while unordered_map needs a hash function and ==. Choose map when you need sorted output or range queries, and unordered_map for pure lookups such as sessions by ID. In both, operator[] inserts a default value for a missing key.
16. What is a function template, and how does the compiler use it?
A function template is a recipe the compiler uses to generate a separate function for each type it is called with. In the snippet, largest(3, 9) deduces T as int and largest(2.5, 1.5) deduces double, so the program prints 9 2.5 mango. Deduction needs all arguments to agree: largest(3, 2.5) fails because T cannot be both int and double, so you cast one argument or name the type explicitly in angle brackets. Without the explicit std::string, two string literals would deduce const char pointers and compare addresses, not text, which is a classic bug.
#include <iostream>
#include <string>
template <typename T>
T largest(T a, T b) {
return (a < b) ? b : a;
}
int main() {
std::cout << largest(3, 9) << " "
<< largest(2.5, 1.5) << " "
<< largest<std::string>("apple", "mango") << "\n";
// largest(3, 2.5); // error: T deduced as int and double
}17. What is a lambda expression, and what does the capture list do?
A lambda is an unnamed function object written inline: a capture list in square brackets, the parameters in round brackets, then the body. The capture list decides which local variables it can use: [passMark] copies the value, [&passMark] refers to the original, and [=] or [&] capture everything used, by copy or by reference. Lambdas shine with algorithms: here one sorts scores in descending order and another counts scores of at least 70, so the program prints 95 3. Prefer explicit captures in a lambda that is stored, because a reference to a dead local dangles.
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {72, 95, 60, 88};
int passMark = 70;
std::sort(scores.begin(), scores.end(),
[](int a, int b) { return a > b; });
auto passed = std::count_if(scores.begin(), scores.end(),
[passMark](int s) { return s >= passMark; });
std::cout << scores[0] << " " << passed << "\n";
}18. How do exceptions work in C++, and why should you catch by reference?
A function signals an error with throw; the stack then unwinds, destroying local objects on the way, until a matching catch block is found, and execution continues there. Here divide(10, 2) prints 5, then divide(1, 0) throws before anything is printed, so the output is 5 and then error: divide by zero. Catch by const reference to a base class, such as const std::exception&. Catching by value copies the object and slices off the derived part, so what() and the real type are lost. Throw standard types like std::invalid_argument or classes derived from them, never raw ints.
#include <iostream>
#include <stdexcept>
int divide(int a, int b) {
if (b == 0) throw std::invalid_argument("divide by zero");
return a / b;
}
int main() {
try {
std::cout << divide(10, 2) << "\n";
std::cout << divide(1, 0) << "\n";
} catch (const std::exception& e) { // by reference
std::cout << "error: " << e.what() << "\n";
}
}19. What is the difference between const int* p and int* const p?
Read the declaration from right to left. const int* p is a pointer to a const int: you cannot change the value through p, but p can point somewhere else. int* const p is a const pointer to an int: the value can change, but the pointer can never be reseated. const int* const p fixes both. In the snippet p1 moves to y and p2 changes x to 5, so it prints 2 5. Const correctness matters because it documents intent: take read-only parameters as const references, and mark member functions that do not modify the object as const so they work on const objects.
#include <iostream>
int main() {
int x = 1, y = 2;
const int* p1 = &x; // pointer to const int
// *p1 = 5; // error: read-only through p1
p1 = &y; // OK: the pointer can move
int* const p2 = &x; // const pointer to int
*p2 = 5; // OK: x becomes 5
// p2 = &y; // error: p2 cannot be reseated
std::cout << *p1 << " " << *p2 << "\n";
}20. When do you pass a parameter by value, by reference or by const reference?
Pass small, cheap types such as int, double, char or a small struct by value. Pass by const reference when you only read a larger object like a std::string or std::vector: it avoids a copy, and the compiler stops you from changing it. Pass by non-const reference only when the function's job is to modify the caller's object, and make that clear in the name, such as normalisePhone(std::string& phone). A modern rule: if the function stores its own copy anyway, take the parameter by value and std::move it into place, so callers passing temporaries pay no extra copy.
Ready to check yourself? Take the free C++ mock test
C++ interview questions: Medium questions (1-3 years)
20 questions21. What are the rule of three, the rule of five and the rule of zero?
Rule of three: if a class needs a custom destructor, copy constructor or copy assignment, it almost always needs all three, because it manages a resource by hand. Rule of five (C++11): add the move constructor and move assignment, because declaring a destructor or a copy operation stops the compiler from generating the moves, so the class silently falls back to copying. Rule of zero, the modern goal: keep resources in members that manage themselves, such as std::string, std::vector and std::unique_ptr, and write none of the five. Only small resource wrappers should need the rule of five.
22. This Buffer class crashes when an object is copied. What is wrong, and how do you fix it?
Buffer owns heap memory through a raw pointer but relies on the compiler-generated copy constructor, which copies the pointer, not the characters. After Buffer b = a, both objects point to the same block, so the second destructor frees it again: a double free, which AddressSanitizer reports as soon as main ends. The best fix is the rule of zero: store the text in a std::string and remove the hand-written destructor. If the class must own raw memory, write a copy constructor and copy assignment that allocate and copy, plus the move operations, or delete copying if a copy makes no sense.
#include <cstring>
class Buffer {
char* data_;
public:
explicit Buffer(const char* s)
: data_(new char[std::strlen(s) + 1]) {
std::strcpy(data_, s);
}
~Buffer() { delete[] data_; }
// No copy operations declared: the defaults copy
// the pointer, not the characters.
};
int main() {
Buffer a("Noida");
Buffer b = a; // b.data_ == a.data_
} // both destructors delete[] the same block23. Why does a polymorphic base class need a virtual destructor?
When you delete a derived object through a base pointer, including through a unique_ptr of the base type, the destructor call is dispatched like any other call. If the base destructor is virtual, the derived destructor runs first and then the base one, so this program prints ~SalesReport and then ~Report. If it is not virtual, the behaviour is undefined, and in practice the derived part is usually not destroyed, leaking whatever it owns. The rule: a class used through base pointers gets a public virtual destructor; a base never deleted that way can use a protected non-virtual one.
#include <iostream>
#include <memory>
struct Report {
virtual ~Report() { std::cout << "~Report\n"; }
};
struct SalesReport : Report {
~SalesReport() override { std::cout << "~SalesReport\n"; }
};
int main() {
std::unique_ptr<Report> r = std::make_unique<SalesReport>();
} // deletes through Report*: both destructors run24. How are virtual functions usually implemented, and what do they cost? Give sizes on 64-bit Linux with GCC.
The standard never mentions vtables, but GCC, Clang and MSVC all use them. Each polymorphic class has one table of function pointers, and each object carries a hidden pointer, the vptr, to its class's table, set by the constructor. A virtual call loads the vptr, picks the slot and makes an indirect call. The costs: on 64-bit Linux with GCC this program prints 4 16, because the 8-byte vptr plus padding grows the object; the call usually cannot be inlined; and the extra memory access can miss the cache. That matters in hot loops over millions of objects and rarely anywhere else.
#include <iostream>
struct Plain { int id; void show() {} };
struct Poly { int id; virtual void show() {} };
int main() {
std::cout << sizeof(Plain) << " " << sizeof(Poly) << "\n";
}25. What happens when a constructor calls a virtual function?
While the Base constructor runs, the Derived part does not exist yet, so the object behaves as a Base and the call goes to Base::init(), not to the override. This program prints Base::init during construction and Derived::init only for the later call. The same applies inside destructors, where the derived part is already gone. This is deliberate: code can never touch derived members before they are initialised. For custom set-up, pass the data to the base constructor or use a separate step after construction. Calling a pure virtual function this way is undefined behaviour.
#include <iostream>
struct Base {
Base() { init(); }
virtual void init() { std::cout << "Base::init\n"; }
virtual ~Base() = default;
};
struct Derived : Base {
void init() override { std::cout << "Derived::init\n"; }
};
int main() {
Derived d; // prints Base::init
d.init(); // prints Derived::init
}26. What is object slicing, and how do you avoid it?
Slicing happens when a derived object is copied into a base object by value: only the base part is copied, and the copy is a real Employee, so virtual calls on it use the base versions. In the snippet byValue(m) prints Employee while byRef(m) prints Manager. It also hits containers: a std::vector of Employee cannot hold managers, only their sliced copies. Avoid it by passing polymorphic objects by reference or pointer and storing them as std::unique_ptr of the base type. Many teams also delete the copy operations of polymorphic base classes, so accidental slicing fails to compile.
#include <iostream>
#include <string>
struct Employee {
virtual std::string role() const { return "Employee"; }
virtual ~Employee() = default;
};
struct Manager : Employee {
std::string role() const override { return "Manager"; }
};
void byValue(Employee e) { std::cout << e.role() << "\n"; }
void byRef(const Employee& e) { std::cout << e.role() << "\n"; }
int main() {
Manager m;
byValue(m); // sliced copy
byRef(m); // no copy
}27. What are lvalues, rvalues and move semantics, and what does std::move actually do?
An lvalue is an object with a name that you can use again, like name; an rvalue is a temporary that is about to disappear, like a string built from a literal. Move semantics let a constructor take over an rvalue's resources, for a string its heap buffer, instead of copying them. std::move itself moves nothing: it is only a cast to an rvalue reference, a promise that the caller no longer needs the value. Here the first push_back copies, the second moves, and the program prints 3 Ananya Krishnan. After a move the source is valid but unspecified, so assign to it before reading it again.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::vector<std::string> batch;
std::string name = "Ananya Krishnan";
batch.push_back(name); // lvalue: copied
batch.push_back(std::move(name)); // cast to rvalue: moved
batch.push_back("Rahul Verma"); // temporary: moved
std::cout << batch.size() << " " << batch[1] << "\n";
name = "next"; // moved-from: assign before reuse
}28. Why should a move constructor be marked noexcept?
When a std::vector grows it must relocate its old elements, and it promises the strong exception guarantee: if anything throws, the vector is unchanged. It can keep that promise while moving only if the move cannot throw, so it moves elements whose move constructor is noexcept and copies the rest when they are copyable. This snippet prints move move; remove noexcept and the same program prints copy copy, which for large objects is a silent performance bug. A defaulted move constructor is noexcept automatically when all members' moves are, so the rule of zero gets this right for free.
#include <iostream>
#include <vector>
struct Order {
Order() = default;
Order(const Order&) { std::cout << "copy "; }
Order(Order&&) noexcept { std::cout << "move "; }
};
int main() {
std::vector<Order> v;
v.reserve(2);
v.emplace_back();
v.emplace_back();
v.emplace_back(); // grows: old elements are relocated
std::cout << "\n";
}29. What is the difference between push_back and emplace_back?
push_back takes an existing object and copies or moves it into the vector. emplace_back forwards its arguments to the element's constructor and builds the object directly in the vector's storage, so team.emplace_back(name, 31) on a vector of Employee skips the temporary. For types that are cheap to move the gain is small. Watch one trap: emplace_back can call explicit constructors, so passing a number to a vector of vectors quietly creates an inner vector of that size, where push_back would refuse to compile. Since C++17, emplace_back also returns a reference to the new element.
30. How do you safely erase elements from a std::vector while looping over it?
Erasing from a vector invalidates iterators at and after the erased element, so the classic bug of calling erase(it) and then ++it uses an invalid iterator: undefined behaviour that may skip elements or crash. The correct loop uses the iterator returned by erase, which points to the next element, and increments only when nothing was erased; this program prints 101 103 105. For many removals, prefer the erase-remove idiom with std::remove_if, or std::erase_if in C++20, which moves each kept element once, O(n) in total, instead of shifting the tail on every erase.
#include <iostream>
#include <vector>
int main() {
std::vector<int> ids = {101, 102, 103, 104, 105};
for (auto it = ids.begin(); it != ids.end(); ) {
if (*it % 2 == 0)
it = ids.erase(it); // returns the next element
else
++it;
}
for (int id : ids) std::cout << id << " ";
std::cout << "\n";
}31. Why prefer STL algorithms to hand-written loops, and what is the classic std::accumulate trap?
Algorithms such as std::sort, std::find_if, std::count_if, std::transform and std::accumulate name the intent, are well tested and are often as fast as or faster than a hand-written loop. The trap: accumulate's result has the type of the initial value, not the element type. With 0 the running sum is an int and every step truncates, so this program prints 359 for wrong and 360 for right. Pass 0.0 for doubles, or 0LL when summing ints into a large total. C++17 also adds std::reduce, which may reorder the work and run in parallel, so its operation must be associative and commutative.
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<double> bills = {99.5, 250.25, 10.25};
auto wrong = std::accumulate(bills.begin(), bills.end(), 0);
auto right = std::accumulate(bills.begin(), bills.end(), 0.0);
std::cout << wrong << " " << right << "\n";
}32. How should a function take a smart pointer as a parameter?
Decide by ownership. If the function only uses the object, take a plain reference, or a raw pointer when null is allowed: it works with any owner and costs nothing. If the function takes ownership, take std::unique_ptr by value, so callers must write std::move and the transfer is visible at the call site. If it keeps a share of ownership, for example storing the object in a cache, take std::shared_ptr by value and move it into place. Avoid taking a shared_ptr by value only to read the object: every copy updates an atomic reference count, which is wasted work on a hot path.
33. How can shared_ptr leak memory, and how does weak_ptr fix it?
If two objects hold std::shared_ptr to each other, such as a Team and its lead Employee, each keeps the other's count above zero, so neither is destroyed even after main drops its own pointers. This reference cycle is a real leak, and LeakSanitizer reports it. The fix is to make the back link a std::weak_ptr, which does not own anything: with it the program prints ~Team and then ~Employee. To use a weak_ptr, call lock(), which returns a shared_ptr that is empty if the object is already gone. Typical uses are parent links in trees, observers and caches.
#include <iostream>
#include <memory>
struct Employee;
struct Team {
std::shared_ptr<Employee> lead;
~Team() { std::cout << "~Team\n"; }
};
struct Employee {
std::weak_ptr<Team> team; // shared_ptr here = leak
~Employee() { std::cout << "~Employee\n"; }
};
int main() {
auto t = std::make_shared<Team>();
auto e = std::make_shared<Employee>();
t->lead = e;
e->team = t;
}34. What are the exception safety guarantees, and how does copy-and-swap help?
There are three levels. Nothrow: the operation never fails, which destructors, swap and move operations should provide. Strong: if it throws, the object is unchanged, like a database commit. Basic: after an exception nothing leaks and the object is valid, but its value may have changed. In setBoth, if copying the skills throws, name_ has already changed, so it gives only the basic guarantee. setBothStrong does the risky copies while building its parameters and then commits with swaps that cannot throw, so it gives the strong guarantee. That is the idea behind the copy-and-swap idiom.
#include <string>
#include <vector>
class Profile {
std::string name_;
std::vector<std::string> skills_;
public:
// Basic guarantee: if the second copy throws,
// name_ has already changed.
void setBoth(const std::string& n,
const std::vector<std::string>& s) {
name_ = n;
skills_ = s;
}
// Strong guarantee: copies are made as arguments,
// then committed with swaps that cannot throw.
void setBothStrong(std::string n, std::vector<std::string> s) {
name_.swap(n);
skills_.swap(s);
}
};35. What is the difference between capturing by value and by reference in a lambda?
A capture by value copies the variable when the lambda is created; a capture by reference refers to the original, so the lambda sees later changes. Here x becomes 5 after both lambdas are made, so the program prints 1 5. Captured copies are const unless the lambda is marked mutable, which the counter uses to keep its own state, so its second call prints 2. The big danger is a reference capture that outlives the variable, for example a lambda stored as a callback or run on another thread after the function has returned: that is a dangling reference.
#include <iostream>
int main() {
int x = 1;
auto byVal = [x] { return x; }; // copy taken now
auto byRef = [&x] { return x; }; // refers to x
x = 5;
std::cout << byVal() << " " << byRef() << "\n";
auto counter = [n = 0]() mutable { return ++n; };
counter();
std::cout << counter() << "\n";
}36. Why are templates usually defined in header files?
The compiler generates a template's code only when it sees the template used with concrete types, and at that point it needs the full definition, not just a declaration. If you put a template's body in a .cpp file, other files that use it compile fine but fail at link time with undefined references, because no file ever instantiated the code for their types. So templates normally live entirely in headers. When the set of types is small and known, you can keep the body in one .cpp file and add explicit instantiation definitions for those types, which also cuts build times.
37. What is a data race, and how do std::thread, std::mutex and std::lock_guard prevent it?
A data race is two threads accessing the same memory at the same time, at least one of them writing, without synchronisation. It is undefined behaviour: increments get lost and results change from run to run. std::thread starts a function on a new thread, and join() waits for it; destroying a std::thread that is still joinable calls std::terminate. A std::mutex lets one thread at a time into a critical section, and std::lock_guard locks it in its constructor and unlocks in its destructor, so here the program reliably prints 200000. In real code, keep the locked section short.
#include <iostream>
#include <mutex>
#include <thread>
int main() {
long total = 0;
std::mutex m;
auto work = [&] {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(m);
++total;
}
};
std::thread t1(work), t2(work);
t1.join();
t2.join();
std::cout << total << "\n";
}38. What is std::string_view in C++17, and what is its main pitfall?
std::string_view is a non-owning view of characters: just a pointer and a length. A function that only reads text can take it by value and accept a std::string, a string literal or a character buffer without copying, and substr() on a view creates no new string. Here domain() returns a view into mail, and the program prints example.in. The pitfall is lifetime: the view does not keep the text alive, so a view of a temporary string, or a returned view of a local string, dangles. Also, a view is not guaranteed to be null-terminated, so never pass data() to a C function that expects a C string.
#include <iostream>
#include <string>
#include <string_view>
std::string_view domain(std::string_view email) {
auto at = email.find('@');
return email.substr(at + 1); // no allocation
}
int main() {
std::string mail = "priya@example.in";
std::cout << domain(mail) << "\n";
// Dangles: the temporary string dies at the semicolon
// std::string_view bad = std::string("x@y.in");
}39. Which C++17 features do you use most in day-to-day code?
Three that make code shorter and safer. Structured bindings unpack pairs, tuples and structs, as in auto [it, inserted] = pins.insert(...). An if with an initializer keeps a variable such as an iterator scoped to the if statement. std::optional returns 'maybe a value' without sentinel values like -1 or out-parameters. This program prints 1 Mysuru and then 302001 0, because value_or(0) supplies the fallback when there is no PIN code for Goa. Others worth naming: std::variant, std::string_view, std::filesystem, inline variables and fold expressions.
#include <iostream>
#include <map>
#include <optional>
#include <string>
std::optional<int> findPin(const std::map<std::string, int>& m,
const std::string& city) {
if (auto it = m.find(city); it != m.end())
return it->second;
return std::nullopt;
}
int main() {
std::map<std::string, int> pins = {{"Jaipur", 302001}};
auto [it, inserted] = pins.insert({"Mysuru", 570001});
std::cout << inserted << " " << it->first << "\n";
std::cout << findPin(pins, "Jaipur").value_or(0) << " "
<< findPin(pins, "Goa").value_or(0) << "\n";
}40. When do you use static_cast, dynamic_cast, const_cast and reinterpret_cast?
static_cast is for well-defined conversions the compiler can check, such as double to int, which truncates 9.99 to 9. dynamic_cast checks the real type at run time on a polymorphic class: for a pointer it returns nullptr on a mismatch and for a reference it throws std::bad_cast, so show() prints savings 3.5 and then not savings. const_cast only adds or removes const, and reinterpret_cast reinterprets bits for low-level work. Avoid C-style casts: when a static_cast is not possible they silently fall back to a const_cast or reinterpret_cast, and they are hard to search for.
#include <iostream>
struct Account { virtual ~Account() = default; };
struct Savings : Account { double rate = 3.5; };
struct Current : Account {};
void show(Account* a) {
if (auto* s = dynamic_cast<Savings*>(a))
std::cout << "savings " << s->rate << "\n";
else
std::cout << "not savings\n";
}
int main() {
Savings s;
Current c;
show(&s);
show(&c);
double d = 9.99;
std::cout << static_cast<int>(d) << "\n"; // 9
}Ready to check yourself? Take the free C++ mock test
C++ interview questions: Hard questions (experienced)
20 questions41. What is perfect forwarding, and why is std::forward needed in a template that takes T&&?
In a function template, a parameter written T&& is a forwarding reference: it binds to both lvalues and rvalues, and T records which one it received. Inside the function, though, the parameter has a name, so it is always an lvalue. std::forward casts it back to an rvalue only when the caller passed an rvalue, so this program prints copy and then move; calling save(value) directly would print copy twice. Wrappers, factories like std::make_unique and emplace functions rely on it. Forward each argument only once, and note that T&& is a forwarding reference only when T is deduced for that call.
#include <iostream>
#include <string>
#include <utility>
void save(const std::string&) { std::cout << "copy\n"; }
void save(std::string&&) { std::cout << "move\n"; }
template <typename T>
void logAndSave(T&& value) { // forwarding reference
save(std::forward<T>(value)); // keeps lvalue or rvalue
}
int main() {
std::string name = "Meera";
logAndSave(name); // copy
logAndSave(std::string("Imran")); // move
}42. In C++20, how do concepts improve on SFINAE and std::enable_if?
A concept is a named compile-time requirement on a type, such as Number here, and a constrained template like twice is considered only for types that satisfy it. With SFINAE you wrote std::enable_if tricks in return types or default template arguments: it worked but was hard to read, and a wrong call gave pages of errors from inside the template. With concepts, calling twice on a string literal fails with a short message that the constraints are not satisfied. Concepts also allow overloading on requirements, and the library ships many, such as std::integral. This program prints 42 2.5.
#include <concepts>
#include <iostream>
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <Number T>
T twice(T x) { return x + x; }
int main() {
std::cout << twice(21) << " " << twice(1.25) << "\n";
// twice("hi"); // error: constraints not satisfied
}43. What are variadic templates and fold expressions?
A variadic template accepts any number of template arguments as a parameter pack, written typename... Ts, and expands it with an ellipsis. Before C++17 you processed a pack by recursion: one overload taking the first argument and the rest, plus a base case. C++17 fold expressions apply an operator across the whole pack in one line: (xs + ... + 0) adds every argument with 0 as the start, so an empty call still compiles, and a comma fold prints each value in order. This program prints 10, then Lucknow 226001 3.5. std::make_unique, std::tuple and emplace_back are all built on variadic templates.
#include <iostream>
template <typename... Ts>
auto total(Ts... xs) {
return (xs + ... + 0); // C++17 fold expression
}
template <typename... Ts>
void printAll(const Ts&... xs) {
((std::cout << xs << ' '), ...); // comma fold, in order
std::cout << '\n';
}
int main() {
std::cout << total(1, 2, 3, 4) << "\n";
printAll("Lucknow", 226001, 3.5);
}44. What can constexpr do in C++17, and how do consteval and constinit in C++20 differ from it?
A constexpr function runs at compile time when its arguments are constant expressions and at run time otherwise. Since C++14 it may contain loops and local variables, so factorial here produces 3628800 for the static_assert and 24 for the array size during compilation, while factorial(n) runs normally; the program prints 24 120. C++20 adds consteval, for functions that must always run at compile time, and constinit, which requires a static variable to be initialised at compile time avoiding static initialisation order problems. Work moved to compile time leaves the hot path.
#include <array>
#include <iostream>
constexpr long long factorial(int n) {
long long r = 1;
for (int i = 2; i <= n; ++i) r *= i;
return r;
}
int main() {
static_assert(factorial(10) == 3628800);
std::array<int, factorial(4)> slots{}; // compile-time size
int n = 5;
std::cout << slots.size() << " " << factorial(n) << "\n";
}45. What is copy elision, and why is return std::move(local) usually a mistake?
Since C++17, returning a prvalue such as Report() is guaranteed to build the object directly in the caller's storage: no copy or move happens, and the type needs no copy or move constructor. Returning a named local uses the named return value optimisation (NRVO), which is allowed but not guaranteed; GCC applies it here even without optimisation, so the program prints ctor | ctor. Writing return std::move(r) makes the operand more than a plain name, which blocks NRVO, so the second half prints ctor move; GCC warns with -Wpessimizing-move. Without NRVO, a returned local is moved anyway.
#include <iostream>
struct Report {
Report() { std::cout << "ctor "; }
Report(const Report&) { std::cout << "copy "; }
Report(Report&&) noexcept { std::cout << "move "; }
};
Report make() { return Report(); } // C++17: guaranteed elision
Report makeNamed() {
Report r;
return r; // NRVO: allowed, not guaranteed
}
int main() {
[[maybe_unused]] Report a = make();
std::cout << "| ";
[[maybe_unused]] Report b = makeNamed();
std::cout << "\n";
}46. Why is std::make_shared usually better than a shared_ptr built from new, and when is it not?
A shared_ptr needs a control block that holds the strong and weak reference counts and the deleter. A shared_ptr built from new allocates the object and the control block separately; make_shared allocates both in one block, which saves an allocation, improves cache locality and keeps bare new out of your code. The trade-off: that single block can be freed only when both counts reach zero, so long-lived weak_ptrs to a large object keep its memory allocated after the object has been destroyed. make_shared also cannot take a custom deleter, and before C++20 it could not create arrays.
47. Is std::shared_ptr thread-safe?
Partly. The reference count is updated atomically, so different threads can copy and destroy their own shared_ptr objects that point to the same target without locking. Two things are not safe. First, the target object: threads that modify it need their own locking. Second, reassigning one shared_ptr variable while another thread reads it is a data race; C++20 adds a std::atomic specialisation for shared_ptr to handle it. The atomic count also has a cost: copying shared_ptrs in a hot loop on many cores makes them fight over one cache line, so pass by reference when not sharing ownership.
48. How do deadlocks happen with two mutexes, and how does std::scoped_lock help?
A deadlock happens when thread 1 holds mutex A and waits for B while thread 2 holds B and waits for A. In a wallet transfer, transfer(a, b) and transfer(b, a) running at the same time lock in opposite orders, which is exactly this case. One fix is a global lock order, such as always locking the account with the smaller ID first. C++17 std::scoped_lock locks several mutexes at once with a deadlock-avoidance algorithm and unlocks them all in its destructor, so this program always finishes and prints 1500 500. Also avoid calling unknown code, such as callbacks, while holding a lock.
#include <functional>
#include <iostream>
#include <mutex>
#include <thread>
struct Account {
std::mutex m;
long balance;
};
void transfer(Account& from, Account& to, long amount) {
std::scoped_lock lock(from.m, to.m); // both or neither
from.balance -= amount;
to.balance += amount;
}
void repeat(Account& from, Account& to, long amount) {
for (int i = 0; i < 500; ++i) transfer(from, to, amount);
}
int main() {
Account a{{}, 1000}, b{{}, 1000};
std::thread t1(repeat, std::ref(a), std::ref(b), 1);
std::thread t2(repeat, std::ref(b), std::ref(a), 2);
t1.join();
t2.join();
std::cout << a.balance << " " << b.balance << "\n";
}49. When is std::atomic enough instead of a mutex, and what does memory_order_relaxed mean?
An atomic makes single operations on one variable, such as fetch_add on a hit counter, indivisible without blocking. That suffices when the shared state is one value. When several values must change together, like a balance and its log, you need a mutex, because two atomic operations are still two separate steps. The default ordering, seq_cst, is the easiest to reason about. memory_order_relaxed gives atomicity with no ordering of other memory, which is fine for a counter read after join(): this prints 400000. Use acquire and release when one thread publishes data for another.
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::atomic<long> hits{0};
std::vector<std::thread> pool;
for (int t = 0; t < 4; ++t)
pool.emplace_back([&hits] {
for (int i = 0; i < 100000; ++i)
hits.fetch_add(1, std::memory_order_relaxed);
});
for (auto& th : pool) th.join();
std::cout << hits.load() << "\n";
}50. How do you use std::condition_variable correctly for a producer-consumer queue?
The consumer holds a std::unique_lock on the queue's mutex and calls wait with a predicate: the queue is not empty or the producer is done. wait releases the mutex while sleeping and locks it again before returning, and the predicate form re-checks the condition in a loop, which handles spurious wake-ups and a notification sent before the consumer started waiting. The producer changes the shared state while holding the same mutex, then calls notify_one or notify_all. Common bugs: waiting without a predicate, setting the flag without the lock, and doing slow work while still holding it.
51. Why is std::vector usually faster than std::list, even for inserts in the middle?
Modern CPUs are far faster than main memory, so performance depends on cache misses more than on Big-O. A vector stores elements contiguously, so the hardware prefetcher streams them into the cache; a list allocates every node separately, and each step follows a pointer to a possibly cold cache line. To insert in the middle of a list you must first walk to the position, which is O(n) pointer chasing, while a vector shifts its tail with fast memory moves. For ints and small structs vector usually wins up to large sizes. Choose list only when you need stable iterators or splicing.
52. How do padding and alignment affect struct size? Give sizes on 64-bit Linux with GCC.
Each type has an alignment, and the compiler inserts padding so every member starts at a multiple of its alignment and the struct size is a multiple of its largest alignment. In Loose, flag takes 1 byte, then 7 bytes of padding so the double starts at offset 8, then grade plus 7 more padding bytes: 24 bytes. Tight puts the double first, then both chars and 6 padding bytes: 16 bytes. On 64-bit Linux with GCC this prints 24 16. For millions of records, ordering members from largest to smallest alignment saves memory and cache. Avoid packed structs unless a file or wire format needs them.
#include <iostream>
struct Loose { char flag; double amount; char grade; };
struct Tight { double amount; char flag; char grade; };
int main() {
std::cout << sizeof(Loose) << " " << sizeof(Tight) << "\n";
}53. A payments service at a Bengaluru fintech became slower after a release. How would you find and fix the problem in its C++ code?
Measure before changing anything. First confirm with production metrics or a benchmark which requests and functions slowed down, and build with the release flags (-O2) plus debug symbols. Then profile: perf record and perf report on Linux, or a flame graph, shows where the CPU time really goes. Typical findings are hidden allocations such as strings and vectors created per call, copies where a move or reference would do, a map lookup in a tight loop, lock contention, or an accidental O(n squared) loop. Fix one thing at a time, measure again, and add a benchmark so the regression cannot return.
54. What do exceptions cost, and what happens if an exception escapes a destructor or a noexcept function?
GCC and Clang use a table-based model: the normal path runs at nearly full speed, at the price of larger binaries for the unwind tables, while an actual throw is far slower than a return because the runtime walks the stack. So exceptions suit rare errors, not normal control flow such as 'item not found'. If an exception leaves a noexcept function, std::terminate is called. Destructors are noexcept by default since C++11, so a destructor that throws also ends the program. Domains such as games and embedded firmware often build with -fno-exceptions and use error codes or std::expected (C++23).
55. What is undefined behaviour, and why can it break code that seems to work?
Undefined behaviour (UB) is code for which the standard places no requirements: signed overflow, reading an uninitialised variable, use after free, out-of-bounds access or a data race. The compiler may assume UB never happens and optimises on that basis, so code that printed the expected value in a debug build can behave differently at -O2; for example, an overflow check written after the overflow can be deleted. So UB is not just 'crashes sometimes'. Catch it with warnings and the undefined behaviour, address and thread sanitizers in CI, and never rely on what one compiler happens to do.
56. What is CRTP, and when would you choose static polymorphism over virtual functions?
In the Curiously Recurring Template Pattern a class derives from a template instantiated with itself, as CsvExporter derives from Exporter of CsvExporter. The base casts this to the derived type and calls its functions directly, so the call is resolved at compile time, can be inlined and needs no vptr; the program prints csv and then pdf. The trade-off: there is no common base type, so you cannot keep CSV and PDF exporters in one vector or pick one at run time, and compile times grow. Use virtual functions for plug-in style variation, and CRTP in hot code where the type is fixed.
#include <iostream>
template <typename Derived>
struct Exporter {
void run() { // no virtual call: resolved at compile time
static_cast<Derived*>(this)->write();
}
};
struct CsvExporter : Exporter<CsvExporter> {
void write() { std::cout << "csv\n"; }
};
struct PdfExporter : Exporter<PdfExporter> {
void write() { std::cout << "pdf\n"; }
};
template <typename T>
void exportAll(Exporter<T>& e) { e.run(); }
int main() {
CsvExporter c;
PdfExporter p;
exportAll(c);
exportAll(p);
}57. Should a function take a callback as std::function or as a template parameter?
Every lambda has its own unique type. A template parameter keeps that type, so the call can be inlined and costs nothing extra, which is how the standard algorithms work; the price is that it must live in a header and is compiled again per callable type. std::function erases the type, so one ordinary function, a class member or a container can hold any callable with the right signature. It costs an indirect call that is usually not inlined, and may allocate on the heap when the callable is too big for its small buffer. Use templates on hot paths and std::function for stored callbacks.
58. How do you apply RAII to a C library handle such as a FILE pointer, and why is a custom deleter better than calling fclose by hand?
Wrap the handle in std::unique_ptr with a custom deleter that calls the library's release function. The deleter then runs on every path out of the scope, whether a normal end, an early return or an exception, so there is no fclose to forget. Here the program prints written and then closed, because the deleter runs when f goes out of scope at the end of main; for a null handle unique_ptr skips the deleter. A stateless deleter type like FileCloser can be optimised away, while a function pointer deleter must be stored in every unique_ptr. The same pattern fits sockets and database handles.
#include <cstdio>
#include <memory>
struct FileCloser {
void operator()(std::FILE* f) const {
std::fclose(f);
std::puts("closed");
}
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
int main() {
FilePtr f(std::tmpfile()); // temporary file
if (!f) return 1; // null: deleter not called
std::fputs("payroll run ok\n", f.get());
std::puts("written");
} // FileCloser runs here, also on early return or throw59. What is the pimpl idiom, and why does its class need an out-of-line destructor?
Pimpl (pointer to implementation) moves a class's private members into an Impl struct that is declared in the header, defined in the .cpp file and held through a std::unique_ptr. Changing the private members then no longer recompiles every file that includes the header, and the layout stays stable for a shared library's ABI. The destructor must be declared in the header and defined in the .cpp after Impl is complete, because unique_ptr's deleter needs the full type; otherwise you get an incomplete-type compile error. The cost: a heap allocation and an extra indirection per access.
60. Is a function-local static (the Meyers singleton) thread-safe, and should you use singletons at all?
Since C++11, initialisation of a function-local static is thread-safe: if several threads call instance() at once, exactly one runs the constructor and the others wait, so no double-checked locking is needed. Only the initialisation is protected, though; later reads and writes of the object's state need their own synchronisation. Singletons are hidden global state: they make unit tests depend on each other and hide a class's real dependencies. Prefer creating the object once in main and passing it by reference, and keep singletons for truly process-wide things like a logger.
Practice questions for November 2026
New every month3 questions from the C++ mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
What does this program print?
int main() { std::string s = "Bengaluru"; s += "-560001"; std::cout << s.size() << " " << s.substr(0, 4) << " " << s.find('l') << "\n"; }- 16 Ben 5
- 16 Beng 5
- 15 Beng 5
- 16 Beng 6
Show the answer
Answer: 16 Beng 5. Bengaluru has 9 characters and -560001 adds 7, so the size is 16. substr(0, 4) takes 4 characters from index 0: Beng. Indexes start at 0, so the first l is at index 5.
-
What does this program print?
class Report { mutable int views = 0; public: void open() const { ++views; } int count() const { return views; } }; int main() { const Report r; r.open(); r.open(); std::cout << r.count() << "\n"; }- It does not compile
- 0
- 1
- 2
Show the answer
Answer: 2. A mutable member may be changed even inside a const member function or on a const object. It is meant for details that do not change the object's visible value, such as a view counter or a cache.
-
Why does this fail to compile?
struct Impl; class Widget { std::unique_ptr<Impl> p; public: Widget(); }; int main() { Widget w; }- The destructor must be declared noexcept(false)
- Impl must have a virtual destructor
- unique_ptr cannot point to a forward-declared type at all
- The implicit destructor is generated where Impl is incomplete, and unique_ptr's deleter needs the complete type
Show the answer
Answer: The implicit destructor is generated where Impl is incomplete, and unique_ptr's deleter needs the complete type. Holding a unique_ptr<Impl> to an incomplete type is fine; deleting it is not. Declare ~Widget(); in the header and define it (even as = default) in the .cpp file after Impl is defined.
Ready to apply?
There are 828 open jobs asking for C++ on CoreHRx right now. Create a free profile once and apply with it.