C++ Interview Notes & Complete Guide
Core Concepts, OOP, Memory Management, Modern C++, STL & Coding Patterns
Table of Contents
C++ Fundamentals & Core Language
What is C++?
C++ is a general-purpose, compiled programming language that supports multiple programming paradigms, including procedural, object-oriented, and generic programming.
One important strength of C++ is that it gives you control over memory and performance while still providing high-level abstractions.
Procedural Programming
Procedural programming is a paradigm based on the concept of procedure calls, where a program is treated as a linear, step-by-step sequence of instructions. It focuses strictly on the actions and functions required to complete a task. Data and the functions that manipulate that data are kept completely separate.
Key Traits: Top-down approach, heavy reliance on global/local variables, and standalone functions.
Real-world Analogy: A cooking recipe. You follow a list of step-by-step instructions (functions) to manipulate raw ingredients (data) until the meal is complete.
Object-Oriented Programming (OOP)
Object-Oriented Programming is a paradigm centered around objects rather than actions. It focuses on the data entities you want to manipulate. Instead of separating data from logic, OOP binds data and its associated behaviors together into cohesive units called classes.
Key Traits: Bottom-up approach, centered on the four pillars: Encapsulation (hiding data), Inheritance (reusing code), Polymorphism (dynamic behavior), and Abstraction (simplifying interfaces).
Real-world Analogy: A smart car. The car contains its internal state (fuel level, speed) and its behaviors (accelerate, brake) all wrapped inside one physical asset. You interact with it through a simple dashboard interface without needing to know how the engine works internally.
How C++ acts as a Procedural Language:
If you want, you can write a massive, complex C++ program without creating a single
class. You can declare global variables, write standalone functions, use structs to
hold plain data, and use pointers to manipulate memory directly just like traditional C code.
How C++ acts as an Object-Oriented Language:
When your project grows large and difficult to manage structurally, you can shift completely to
OOP. C++ gives you the class keyword, private/public
access specifiers to lock down data security, inheritance hierarchies to extend code, and
virtual functions to achieve runtime polymorphism.
By functioning as a hybrid language, C++ lets developers write high-level object-oriented architectures for application design while dropping down into procedural, low-level optimization routines exactly where performance matters most.
C++ supports multiple programming paradigms:
Procedural → functions, loops, conditions Object-oriented → classes, inheritance, polymorphism Generic → templates Functional style → lambdas, algorithms
Why is C++ called a middle-level language?
C++ is sometimes informally called a middle-level language because it provides both high-level abstractions such as classes and templates and low-level features such as pointers, manual memory management, and direct memory manipulation.
What is the difference between C and C++?
C is primarily a procedural programming language, while C++ supports procedural, object-oriented, and generic programming. C++ provides features such as classes, inheritance, polymorphism, function/operator overloading, templates, exceptions, and the STL that are not part of C.
What are namespaces in C++?
A namespace is a named scope used to organize identifiers and prevent naming
conflicts. The C++ standard library is inside the std namespace.
Its primary purpose is name organization and avoiding naming conflicts.
Simple explanation
Suppose two libraries both have a function called print().
Without namespaces:
print();
Which print() should be called?
Namespaces allow us to separate them:
namespace A {
void print() {
cout << "A";
}
}
namespace B {
void print() {
cout << "B";
}
}
Now:
A::print();
B::print();
The :: operator is called the scope resolution operator.
Code example
#include <iostream>
namespace First {
int value = 10;
}
namespace Second {
int value = 20;
}
int main() {
std::cout << First::value << '\n';
std::cout << Second::value << '\n';
return 0;
}
10
20
What is std?
std is the namespace containing entities provided by the C++ standard library.
For example:
std::vector
std::string
std::cout
std::map
std::unique_ptr
What does using namespace std; do?
It allows you to omit std::
using namespace std;
vector<int> nums;
cout << "Hello";
instead of:
std::vector<int> nums;
std::cout << "Hello";
What are the different data types in C++?
C++ data types can broadly be categorized into fundamental types, derived types, user-defined types, and enumeration types.
1. Fundamental types
Examples:
int
char
float
double
bool
void
Some modifiers include:
short
long
signed
unsigned
Example:
int age = 21;
double salary = 50000.5;
char grade = 'A';
bool passed = true;
2. Derived types
Examples:
int arr[5];
int* ptr;
int& ref = x;
Common derived types:
- Arrays
- Pointers
- References
- Function types
3. User-defined types
Examples:
class Student {};
struct Employee {};
Also:
union Data {};
4. Enumeration
enum class Color {
RED,
GREEN,
BLUE
};
| Data Type | Typical Size | Minimum Guaranteed | Value Range (Signed) | Value Range (Unsigned) |
|---|---|---|---|---|
bool |
1 Byte | 1 Byte | true (1) or false (0) | N/A |
char |
1 Byte | 1 Byte | -128 to 127 | 0 to 255 |
| short | 2 Bytes | 2 Bytes | -32,768 to 32,767 | 0 to 65,535 |
int |
4 Bytes | 2 Bytes | -2,147,483,648 to 2,147,483,647 | 0 to 4,294,967,295 |
| long | 4 or 8 Bytes* | 4 Bytes | -2B to 2B (Win) or -9B to 9B (Linux) | 0 to 4B (Win) or 0 to 18B (Linux) |
| long long | 8 Bytes | 8 Bytes | -9,223,372,036,854,775,808 to ... | 0 to 18,446,744,073,709,551,615 |
For any signed type with n bits, the range is always -2n−1 to 2n−1 − 1.
| Data Type | Typical Size | Precision | Breakdown (Sign + Exponent + Mantissa) |
|---|---|---|---|
float |
4 Bytes (32 bits) | ~7 decimal digits | 1 sign bit + 8 exponent bits + 23 mantissa bits |
double |
8 Bytes (64 bits) | ~15 decimal digits | 1 sign bit + 11 exponent bits + 52 mantissa bits |
long double |
8, 12, or 16 Bytes* | ~19 decimal digits | Varies by architecture (often an 80-bit extended format) |
The void Type
- Size: 0 Bytes (Incomplete type).
- Precision: N/A
Class vs. Struct
- Default Access: In a
class, all members areprivateby default. In astruct, all members arepublicby default. - Inheritance Access: Inheritance defaults to
privatefor classes andpublicfor structs. - Convention: Developers use
structfor plain data records andclasswhen writing complex systems withprivatestates and member functions.
Struct vs. Union
An union is a special user-defined data type in C++ where all members share
the exact same memory location.
Unlike a struct or a class, which allocates separate memory space for
every single member, a union allocates only enough memory to hold its
largest member. This means a union can store the value of
only one member at a time. Writing data to a new member automatically
overwrites the data of the previous one.
Imagine you have a variable that needs to hold either an int (4 bytes) OR a
double (8 bytes), but never both at the same time.
- A
structcreates separate slots: It takes 12 bytes (4 + 8) plus alignment padding. - A
unionoverlays the slots: It takes only 8 bytes (the size of thedouble). The integer just sits at the beginning of those same 8 bytes.
Enums
An enum (short for enumeration) is a user-defined data type in C++ used to assign
human-readable names to integral constants.
Instead of using vague, unreadable numbers (like 0, 1, 2) to represent states in your code, an
enum allows you to use meaningful words (like Low, Medium, High), making your code
self-documenting and much easier to maintain.
Introduced in C++11, enum class is the modern standard
used in production environments because it fixes the security flaws of traditional enums.
#include <iostream>
enum class Status {
Pending = 10, // You can explicitly assign start values
Approved, // Automatically becomes 11
Rejected // Automatically becomes 12
};
int main() {
// 1. Scoped Scope: You MUST use the 'Status::' prefix
Status current = Status::Pending;
// 2. Strong Type Safety: This line will cause a COMPILER ERROR:
// if (current == 10) { }
// To compare against an integer, you must explicitly typecast it:
if (static_cast<int>(current) == 10) {
std::cout << "Status is pending!\n";
}
std::cout << "Size of enum: " << sizeof(current) << " bytes\n"; // Defaults to 4 bytes (int)
return 0;
}
What is a class and what is an object?
A class is a user-defined type that defines data and behavior, while an object is an
instance of that class. The class defines the structure, and the
object represents an actual instance with its own state.
Simple explanation
Think of:
Class → Blueprint Object → Actual instance
Example:
class Car {
public:
string brand;
void drive() {
cout << "Car is driving\n";
}
};
Create objects:
Car car1;
Car car2;
Both objects belong to the same class but can have different state:
car1.brand = "BMW";
car2.brand = "Audi";
Does a class occupy memory?
The class definition itself does not represent an object instance with per-object
storage.
When objects are created, memory is allocated for their non-static data
members, along with any implementation-specific overhead such as a
vptr for a polymorphic object.
Static data members have separate storage and are not duplicated for each object.
Don't simply say:
Class takes zero memory.
That's too absolute because class-related entities can have storage, such
as static data members, and objects can have implementation-specific overhead.
What are access specifiers in C++?
C++ provides three main access specifiers: public, private, and
protected. They control which parts of a class can access its members.
public
Accessible from anywhere where the object is accessible.
class A {
public:
int x;
};
private
Accessible only from within the class and its friends.
class A {
private:
int x;
};
protected
Accessible from:
- The
classitself - Friend declarations
- Derived classes
class A {
protected:
int x;
};
Quick table
| Access | Class | Derived class | Outside |
|---|---|---|---|
public |
Yes | Yes | Yes |
protected |
Yes | Yes | No |
private |
Yes | No* | No |
* A derived class cannot directly access a base class's
private members, though friends and public/protected
member functions can provide access.
What is the default access in a class?
class → private
struct → public
In C++, the default access specifier for a class is
private. This means that if you do not explicitly label your
variables or functions with an access modifier, they cannot be accessed or modified from outside
the class.
What is encapsulation in C++?
Encapsulation is the process of combining data members and member functions into a single unit
(class) while restricting direct access to the data.
Usually, we make data members private and provide public methods to
access or modify them.
Example
class BankAccount {
private:
double balance = 0;
public:
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
double getBalance() const {
return balance;
}
};
Outside code cannot directly do:
account.balance = -1000;
because balance is private.
Instead, it must use:
account.deposit(1000);
Why is encapsulation useful?
It provides:
- Data protection
- Controlled modification
- Better maintainability
- Clear interfaces
- Reduced coupling
Don't say:
Encapsulation means only making variables private.
Private members are one mechanism used to achieve encapsulation. Encapsulation is the broader design concept.
What is abstraction in C++?
Abstraction means exposing only the necessary functionality while hiding unnecessary implementation details from the user.
Abstraction means: Show what an object can do, but hide how it does it.
Real-world example:
When using an ATM, you know:
withdraw();
deposit();
checkBalance();
But you don't need to know how the bank internally processes the transaction.
Simple example
Consider:
class Car {
public:
void start() {
// Complex internal operations
// fuel system
// ignition
// engine control
// etc.
}
};
The user only needs:
car.start();
They don't need to know every internal step involved in starting the engine.
That's abstraction.
Abstraction in C++
In C++, one common way to achieve abstraction is using:
Abstract class
Pure virtual function
Abstract Classes:
An abstract class is a class that contains at least one pure
virtual function. It serves as a base class that defines a common
interface while leaving the implementation of specific operations to derived classes.
- An abstract
classcontains at least one purevirtualfunction. - Objects of an abstract
classcannot be instantiated. - Derived classes must implement the pure
virtualfunctions. - It provides a common interface to achieve abstraction.
In C++, abstraction can be achieved using abstract classes and pure virtual
functions.
#include <iostream>
// 1. ABSTRACT CLASS
class Animal {
public:
// Pure virtual function makes this class abstract
virtual void makeSound() = 0;
void sleep() {
std::cout << "Sleeping... zzz" << std::endl;
}
};
// 2. CONCRETE DERIVED CLASS
class Dog : public Animal {
public:
// Fulfilling the contract by implementing the pure virtual function
void makeSound() override {
std::cout << "Woof! Woof!" << std::endl;
}
};
int main() {
// -------------------------------------------------------------
// LINE BELOW WILL FAIL TO COMPILE:
// Error: cannot declare variable 'myAnimal' to be of abstract type 'Animal'
// -------------------------------------------------------------
// Animal myAnimal;
// -------------------------------------------------------------
// VALID USAGE:
// You cannot create an instance, but you CAN use pointers or
// references of the abstract class type to point to a concrete object.
// -------------------------------------------------------------
Animal* polyDog = new Dog();
polyDog->makeSound(); // Outputs: Woof! Woof!
polyDog->sleep(); // Outputs: Sleeping... zzz
delete polyDog;
return 0;
}
Abstract class example
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
Shape defines what derived classes must provide without defining the complete implementation.
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
What is the difference between abstraction and encapsulation?
Remember:
Encapsulation
→ How data and behavior are packaged/protected
Abstraction
→ What details are exposed/hidden
Don't say:
Abstraction means hiding all data.
That's closer to access control/encapsulation.
Abstraction hides unnecessary implementation complexity while exposing a useful interface.
What is a function in C++?
A function is a named block of code that performs a specific operation. It can accept parameters and optionally return a value. Functions help with modularity, code reuse, and readability.
Example:
int add(int a, int b) {
return a + b;
}
Calling:
int result = add(10, 20);
What is the difference between a function declaration and definition?
Declaration:
int add(int, int);
Definition:
int add(int a, int b) {
return a + b;
}
The declaration tells the compiler about the function.
The definition provides its implementation.
What is function overloading?
Function overloading means defining multiple functions with the same name but different parameter lists. The compiler determines which function to call based on the arguments, so it is compile-time polymorphism.
Example
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
};
Now:
Calculator c;
c.add(2, 3);
c.add(2.5, 3.5);
c.add(1, 2, 3);
The compiler chooses the appropriate overload.
What can differ?
Parameters can differ by:
- Number
- Type
- Order
Can we overload a function only by changing its return type?
No.
This is invalid:
int getValue();
double getValue();
The compiler cannot distinguish functions based only on return type.
Wrong:
Function overloading happens at runtime.
Correct:
Function overloading is resolved at compile time.
What is operator overloading?
Operator overloading allows us to define how an existing operator behaves for user-defined types such as classes.
For example, we can define what + means for two Complex objects.
Example
class Complex {
private:
double real;
double imag;
public:
Complex(double r, double i)
: real(r), imag(i) {}
Complex operator+(const Complex& other) const {
return Complex(
real + other.real,
imag + other.imag
);
}
};
Now:
Complex a(2, 3);
Complex b(4, 5);
Complex c = a + b;
The expression:
a + b
uses our overloaded operator+.
Can every operator be overloaded?
No.
Some operators cannot be overloaded, such as:
::
.
.*
?:
sizeof
Operator overloading does not create a new operator.
You can only redefine the behavior of operators that C++ already provides, for appropriate user-defined types.
What is an inline function?
An inline function is a function for which the programmer allows the compiler to
perform inline substitution, potentially reducing function-call overhead. However,
inline does not guarantee that the compiler will actually inline the
function.
Example
inline int square(int x) {
return x * x;
}
Calling:
int x = square(5);
The compiler may optimize it as if the function body were placed directly at the call site.
Does inline always improve performance?
No.
Inlining can reduce function-call overhead, but excessive inlining can increase code size and potentially hurt instruction-cache behavior.
Modern compilers make their own optimization decisions.
❌:
inline guarantees the function will be expanded inline.
Correct:
inline does not guarantee actual inlining.
What is static in C++?
static has different meanings depending on where it is used.
For a local variable, it extends the variable's lifetime to the entire program while
preserving local scope. For a class data member, it creates one shared member for
the class rather than one copy per object.
A static member function belongs to the class rather than a particular
object.
Static local variable
void counter() {
static int count = 0;
count++;
cout << count << '\n';
}
Calling:
counter();
counter();
counter();
outputs:
1
2
3
The variable retains its value between calls.
Static class member
class Student {
public:
static int count;
Student() {
count++;
}
};
int Student::count = 0;
If we create:
Student s1;
Student s2;
Student s3;
there is one shared count, not three separate count variables.
Static member function
class Student {
public:
static int count;
static void showCount() {
cout << count;
}
};
Call:
Student::showCount();
No object is required.
A static member function does not have a this pointer because it is
not associated with a particular object.
Therefore it cannot directly access non-static members.
Can a static function access a non-static member?
Not directly.
class A {
int x;
public:
static void fun() {
// cout << x; // Error
}
};
There is no particular object from which to obtain x.
Don't say:
Static means the variable is stored on the heap.
That's incorrect.
Storage location is an implementation detail and depends on the context.
Friend Keyword
In C++, the friend keyword is used to grant non-member functions or
external classes full access to a class's private and
protected members
What is a friend function?
A friend function is a non-member function that is explicitly granted access to a
class's private and protected members using the
friend declaration.
Example
class Box {
private:
int value;
public:
Box(int v) : value(v) {}
friend void printValue(const Box& box);
};
void printValue(const Box& box) {
cout << box.value;
}
Although printValue() is not a member function, it can access:
box.value
because it is declared as a friend.
A friend function:
- Is not a member function
- Does not have a
thispointer - Can access
private/protectedmembers - Must be explicitly declared as a
friend
Does friendship get inherited?
No, friendship is not inherited.
Also, friendship is not automatically transitive.
If A is friend of B, that doesn't automatically mean A is friend
of C.
Don't say:
Friend functions are member functions with special permissions.
They are non-member functions.
What is a friend class?
A friend class is a class that is granted access to the
private and protected members of another class.
Example
class Engine {
private:
int power = 100;
friend class Car;
};
class Car {
public:
void showEnginePower(const Engine& engine) {
cout << engine.power;
}
};
Car can access Engine::power because Car is a friend of Engine.
Is friendship mutual?
No.
If:
class A {
friend class B;
};
then B can access A's private members.
It does not mean A can access B's private members.
Friendship should generally be used carefully because it increases coupling between classes.
What is the this pointer?
this is a pointer available inside a non-static member
function that points to the object on which the function was called.
Example
class Student {
private:
int age;
public:
void setAge(int age) {
this->age = age;
}
};
Here we have two variables called age:
parameter age
member age
So:
this->age
means the object's member variable.
Example of method chaining
class Student {
private:
int age;
public:
Student& setAge(int age) {
this->age = age;
return *this;
}
};
Then:
Student s;
s.setAge(20).setAge(21);
this is not available in static member functions
because static functions are not associated with a particular object.
What is the type of this?
For a non-const member function of:
class A
conceptually:
A* const
For a const member function:
const A* const
The exact type has subtleties, but for a fresher interview, understanding that this
points to the current object is the important part.
Don't say:
this contains the object's value.
It contains the address of the current object.
What is a reference in C++?
A reference is an alias for an existing object or variable. It must normally be initialized when declared and cannot later be reseated to refer to another object.
Example
int x = 10;
int& ref = x;
ref = 20;
cout << x;
20
Both:
x
and:
ref
refer to the same object.
Pointer vs reference
| Pointer | Reference |
|---|---|
| Can be null | Normally cannot be null |
| Can be reassigned | Cannot be reseated |
Uses * / -> |
Used like the original variable |
| Can support pointer arithmetic | No pointer arithmetic |
| Can represent absence | Normally represents an existing object |
Example
Pointer:
int x = 10;
int y = 20;
int* p = &x;
p = &y;
Reference:
int x = 10;
int y = 20;
int& ref = x;
// ref = y;
// This assigns y's value to x;
// it does NOT make ref refer to y.
Can we create an array of references?
No. C++ does not allow arrays whose elements are references.
You can have:
int* arr[5];
but not:
int& arr[5]; // invalid
What is call by value vs call by reference?
In pass-by-value, the function receives a copy of the argument, so modifying the parameter does not modify the original object.
In pass-by-reference, the function operates on the original object through a reference, so modifications can affect the caller's object.
Pass by value
void increment(int x) {
x++;
}
int main() {
int n = 10;
increment(n);
cout << n;
}
10
Because x is a copy.
Pass by reference
void increment(int& x) {
x++;
}
int main() {
int n = 10;
increment(n);
cout << n;
}
11
For read-only large objects
A very common C++ pattern is:
void print(const string& s) {
cout << s;
}
This avoids copying the string while preventing modification.
Why use const T&?
To avoid copying a potentially large object while also preventing the function from modifying it.
Don't say:
Pass by reference always passes the address.
That's an implementation-level explanation and isn't the best language-level definition.
A reference provides an alias to the original object.
What is a pointer?
A pointer is an object that stores the address of another object or function. Pointers can be dereferenced to access the object they point to.
Example
int x = 10;
int* p = &x;
cout << *p;
10
Here:
&x → address of x p → stores that address *p → accesses the object at that address
Pointer can be changed
int x = 10;
int y = 20;
int* p = &x;
p = &y;
Now p points to y.
What is a null pointer?
A pointer that doesn't point to an object/function.
Modern C++:
int* p = nullptr;
Don't dereference:
int* p = nullptr;
cout << *p; // Undefined behavior
What is nullptr?
nullptr is the null pointer literal introduced in C++11. It represents a null
pointer and has type std::nullptr_t. It is preferred over the old NULL or integer
literal 0 because it provides better type safety.
Example
int* p = nullptr;
if (p == nullptr) {
cout << "Null pointer";
}
Why is nullptr better than NULL?
Consider overloaded functions:
void fun(int);
void fun(int*);
With:
fun(NULL);
depending on how NULL is defined, the call can resolve to the integer overload.
But:
fun(nullptr);
selects the pointer overload.
What is the type of nullptr?
Conceptually:
std::nullptr_t
❌ Don't say:
nullptr is an integer containing zero.
It is a null pointer literal, not an integer.
What is const in C++?
const specifies that an object or value cannot be modified through that particular
declaration. It can be applied to variables, pointers, references, member functions, parameters,
and return types.
Const variable
const int x = 10;
// x = 20; // Error
Pointer to const
const int* p;
You cannot modify the integer through p.
*p = 20; // Error
But p itself can point somewhere else.
Const pointer
int* const p = &x;
The pointer cannot point somewhere else, but the pointed object can be modified through p.
Both const
const int* const p = &x;
Neither the pointer nor the pointed value can be modified through p.
Const member function
class Student {
public:
int getAge() const {
return age;
}
private:
int age;
};
A const member function tells the compiler that the function will not change any
non-static data members of the object.
Why use const in function parameters?
For example:
void print(const string& name);
It means:
- No unnecessary copy
- Function cannot modify name
This is a very common and important C++ pattern.
Remember the difference:
const int* p;
means pointer to const int.
int* const p;
means const pointer to int.
What is a pointer to const?
const int x = 10;
const int* p = &x;
or:
int x = 10;
const int* p = &x;
You cannot modify x through p:
*p = 20; // ❌
But p itself can point somewhere else:
int y = 20;
p = &y; // ✅
What is a const pointer?
int x = 10;
int y = 20;
int* const p = &x;
Here the pointer itself is constant.
So:
*p = 30; // ✅
but:
p = &y; // ❌
What is a const pointer to const data?
const int* const p = &x;
Now neither can change through p:
*p = 20; // ❌
p = &y; // ❌
Why do we use const member functions?
They allow read-only operations on const objects and communicate that the function
should not modify the object.
Example:
class Account {
double balance;
public:
double getBalance() const {
return balance;
}
void deposit(double amount) {
balance += amount;
}
};
Then:
const Account account;
account.getBalance(); // ✅
account.deposit(100); // ❌
This is especially important when designing class interfaces.
What are default arguments in C++?
A default argument is a value automatically used by a function when the caller does not provide that argument.
Simple explanation:
You can give a function parameter a default value.
void greet(string name = "Dilip") {
cout << "Hello " << name;
}
greet(); // Hello Dilip
greet("Rahul"); // Hello Rahul
If the argument is not passed, "Dilip" is used.
Default arguments should generally be given from right to left.
void fun(int a, int b = 10, int c = 20); // Valid
But:
void fun(int a = 10, int b); // Invalid
because b has no default value after a defaulted parameter.
Where should default arguments be specified: declaration or definition?
Usually, default arguments are specified in the function declaration, especially when the declaration is in a header file. They generally should not be repeated in the definition.
// Declaration
void print(int x, int y = 10);
// Definition
void print(int x, int y) {
cout << x << " " << y;
}
Then:
print(5); // y = 10
print(5, 20); // y = 20
What is scope in C++?
Scope defines the region of a program where a name, such as a variable or function, can be accessed.
Simple explanation:
Different types of scope include:
- Local/block scope
- Function parameter scope
- Class scope
- Namespace scope
- Global scope
Example:
int global = 10; // global scope
void fun() {
int x = 20; // local scope
{
int y = 30; // block scope
cout << y;
}
// cout << y; // ❌ y is out of scope
}
What is the difference between scope and lifetime?
Scope tells where a variable can be accessed, while lifetime tells how long the variable exists in memory.
Example:
void fun() {
int x = 10;
}
x:
- Scope → inside
fun() - Lifetime → from its initialization until
fun()exits
Scope and lifetime are related but not the same thing.
A static local variable is a good example:
void counter() {
static int x = 0;
x++;
cout << x;
}
Its scope is inside counter(), but its lifetime is the entire
program.
What is a pointer to a pointer?
A pointer to a pointer stores the address of another pointer.
int x = 10;
int* p = &x;
int** pp = &p;
cout << **pp; // 10
Relationship:
x ↑ p ↑ pp pp → address of p *pp → p **pp → x
Common use cases:
- Dynamic 2D arrays
- Modifying a pointer inside a function
- Low-level data structures
- C-style APIs
OOP, Constructors & Destructors
What is a constructor?
A constructor is a special member function that is automatically called when an object is created. It is mainly used to initialize the object's state.
Simple explanation:
class Student {
int age;
public:
Student() {
age = 20;
}
};
int main() {
Student s; // Constructor automatically called
}
The constructor:
- Has the same name as the
class. - Has no return type.
- Runs automatically during object initialization.
- Can be overloaded.
Can a constructor return a value?
Answer: No. Constructors do not have a return type.
Don't say a constructor "creates memory for the object." Object storage is determined by how the object is created; the constructor initializes the object.
Why do we need constructors?
Constructors ensure that an object starts in a valid and properly initialized state.
Without a constructor:
class Account {
int balance;
};
We could accidentally have an uninitialized balance for a default-initialized object.
With a constructor:
class Account {
int balance;
public:
Account(int initialBalance) : balance(initialBalance) {}
};
Now:
Account a(1000);
The object starts with a valid balance.
What is a default constructor?
A default constructor is a constructor that can be called without arguments.
Example:
class Student {
public:
Student() {
cout << "Default constructor";
}
};
Student s;
It can also have default parameters:
class Student {
public:
Student(int age = 18) {}
};
Student s; // Valid
A default constructor does not necessarily mean a constructor literally
written as ClassName().
This is also a default constructor:
Student(int age = 18);
because it can be called with no arguments.
What is a parameterized constructor?
A parameterized constructor accepts arguments to initialize an object with specific values.
class Student {
string name;
int age;
public:
Student(string n, int a) : name(n), age(a) {}
};
Student s("Dilip", 21);
Each object can have different initial values.
What is constructor overloading?
Constructor overloading means defining multiple constructors with different parameter lists in
the same class.
class Student {
public:
Student() {}
Student(int age) {}
Student(string name, int age) {}
};
Now:
Student a;
Student b(21);
Student c("Dilip", 21);
The compiler chooses the appropriate constructor based on the arguments.
Is constructor overloading compile-time polymorphism?
Yes. It is a form of function overloading and therefore compile-time polymorphism.
What is a copy constructor?
A copy constructor creates a new object by copying an existing object of the same
class.
Typical form:
class Student {
public:
Student(const Student& other) {
// copy data
}
};
Example:
Student s1;
Student s2 = s1;
Here s2 is initialized using the copy constructor.
Why const Student&?
Because:
- We don't want to modify s1.
- Passing by reference avoids another copy.
- Passing by value would require copying again.
Copy constructor:
Student(const Student&);
Copy assignment:
Student& operator=(const Student&);
They are different operations.
What is a move constructor?
A move constructor transfers resources from a temporary or expiring object instead of copying them.
Typical form:
class Buffer {
public:
Buffer(Buffer&& other) {
// take ownership of other's resources
}
};
Example:
Buffer a;
Buffer b = std::move(a);
Instead of duplicating expensive resources, ownership can be transferred.
Why is this useful?
Suppose an object owns a large dynamically allocated buffer.
Copying:
Object A ↓ Large resource Object B ↓ New copy of resource
Moving:
Object A ↓ resource ownership transferred ↓ Object B
This can significantly improve performance.
Constructor vs normal function
| Constructor | Normal function |
|---|---|
Same name as class |
Any valid name |
| No return type | Has return type |
Can a constructor be virtual?
No, constructors cannot be virtual.
Reason:
Virtual dispatch requires an already-existing object, but the constructor is responsible for initializing that object.
class Base {
public:
virtual Base() {} // ❌ Invalid
};
Destructors can be virtual, but constructors cannot.
Can a constructor be private?
Yes, a constructor can be private. This is commonly used to control object creation,
such as in the Singleton pattern or factory-style designs.
class Singleton {
private:
Singleton() {}
public:
static Singleton& getInstance() {
static Singleton obj;
return obj;
}
};
External code cannot directly do:
Singleton s; // ❌
because the constructor is private.
Other use cases:
- Factory methods
- Controlled object creation
- Singleton-like designs
What is a delegating constructor?
A delegating constructor calls another constructor of the same class to avoid
duplicate initialization logic.
class Student {
string name;
int age;
public:
Student() : Student("Unknown", 18) {}
Student(string n, int a)
: name(n), age(a) {}
};
Here:
Student()
delegates to:
Student(string, int)
Benefit:
Avoids duplicating initialization code.
What is an initializer list?
An initializer list initializes data members before the constructor body executes.
class Student {
string name;
int age;
public:
Student(string n, int a)
: name(n), age(a) {
}
};
The part:
: name(n), age(a)
is the initializer list.
Why should we prefer initializer lists?
Initializer lists directly initialize members and are required for certain members such as
references, const members, and members without default constructors.
Example:
class Example {
const int x;
int& ref;
public:
Example(int value, int& r)
: x(value), ref(r) {}
};
You cannot properly initialize these members through normal assignment in the constructor body.
Also, for class-type members:
class Person {
string name;
public:
Person(string n) : name(n) {}
};
The string is directly initialized instead of first being default-constructed and then assigned.
Initialization vs assignment
Initialization:
class A {
int x;
public:
A(int value) : x(value) {}
};
x is initialized.
Assignment:
class A {
int x;
public:
A(int value) {
x = value;
}
};
x is initialized first and then assigned a new value.
For primitive types the performance difference may be small, but for class-type
members it can matter.
Initialization creates the member with the desired value, while assignment changes the value of an already-existing object.
What is a destructor?
A destructor is a special member function that is automatically called when an object's lifetime ends. It is used for cleanup and resource release.
Syntax:
class Student {
public:
~Student() {
cout << "Destructor called";
}
};
For:
Student s;
the destructor is automatically called when s goes out of scope.
Can destructors be overloaded?
No. A class can have only one destructor because a destructor takes no parameters.
Invalid:
~Student();
~Student(int x); // ❌
There is only:
~Student();
Can a destructor take parameters?
No. A destructor cannot take parameters and cannot have a return type.
~Student(); // ✅
~Student(int x); // ❌
This is also why destructors cannot be overloaded.
Can a destructor be virtual?
Yes. A destructor can be virtual, and a base class intended for
polymorphic use should generally have a virtual destructor.
class Base {
public:
virtual ~Base() {}
};
Why do we need a virtual destructor?
A virtual destructor ensures that deleting a derived object through a
base-class pointer calls both the derived and base destructors.
Example:
class Base {
public:
virtual ~Base() {
cout << "Base destructor\n";
}
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived destructor\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr;
}
Derived destructor
Base destructor
Without a virtual destructor, deleting a derived object through a base pointer
results in undefined behavior.
Very common interview question:
Base* p = new Derived();
delete p;
Ask yourself:
Is Base::~Base() virtual?
If yes → proper derived destruction.
What is the constructor/destructor calling order?
Construction happens from base to derived, while destruction happens in the reverse order.
For:
class A {};
class B : public A {};
class C : public B {};
Construction:
Destruction:
What is the base constructor vs derived constructor order?
The base class constructor always executes before the derived class
constructor.
class Base {
public:
Base() {
cout << "Base\n";
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived\n";
}
};
Creating:
Derived d;
Base
Derived
What is the base destructor vs derived destructor order?
The derived destructor executes first, followed by the base destructor.
Construction:
Base → Derived
Destruction:
Derived → Base
This guarantees that the derived part of the object is cleaned up before the base portion.
What happens when an exception occurs during construction?
If a constructor throws an exception, the object is considered not successfully constructed, and destructors are called for the already-constructed subobjects and base classes.
What is inheritance?
Inheritance allows a derived class to acquire the data members and member functions
of a base class.
class Animal {
public:
void eat() {
cout << "Eating\n";
}
};
class Dog : public Animal {
public:
void bark() {
cout << "Barking\n";
}
};
Now:
Dog d;
d.eat();
d.bark();
Dog inherits from Animal.
What are the types of inheritance?
The commonly discussed types are:
- Single inheritance
- Multiple inheritance
- Multilevel inheritance
- Hierarchical inheritance
- Hybrid inheritance
What is single inheritance?
One derived class inherits from one base class.
class Animal {};
class Dog : public Animal {};
Animal ↓ Dog
What is multiple inheritance?
A derived class inherits from multiple base classes.
class Printer {};
class Scanner {};
class Machine : public Printer, public Scanner {};
Printer Scanner
\ /
\ /
Machine
Multiple inheritance can introduce ambiguity, especially when both base classes contain members with the same name.
What is multilevel inheritance?
In multilevel inheritance, a class derives from another derived class.
class A {};
class B : public A {};
class C : public B {};
A ↓ B ↓ C
What is hierarchical inheritance?
Multiple derived classes inherit from the same base class.
class Animal {};
class Dog : public Animal {};
class Cat : public Animal {};
class Horse : public Animal {};
Animal
/ | \
Dog Cat Horse
What is hybrid inheritance?
Hybrid inheritance is a combination of two or more types of inheritance.
For example, a combination of:
- Multiple inheritance
- Multilevel inheritance
The classic diamond problem is an example where hybrid inheritance can create ambiguity.
What is polymorphism?
Polymorphism means "many forms."
It allows the same interface or operation to behave differently depending on the context or object.
Two major types:
Polymorphism ├── Compile-time └── Runtime
Examples:
add(2, 3);
add(2.5, 3.5); // overloading
and:
Base* p = new Derived();
p->show(); // runtime polymorphism
What is compile-time polymorphism?
Compile-time polymorphism is resolved by the compiler, mainly through function overloading, operator overloading, and templates.
Example:
void print(int x) {
cout << x;
}
void print(string x) {
cout << x;
}
The compiler determines which print() to call.
What is runtime polymorphism?
Runtime polymorphism occurs when the function to execute is determined at runtime using
virtual functions and overriding.
class Animal {
public:
virtual void sound() {
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void sound() override {
cout << "Bark\n";
}
};
Animal* a = new Dog();
a->sound(); // Bark
The base pointer points to a derived object, and the overridden function is selected at runtime.
Overloading vs overriding
| Overloading | Overriding |
|---|---|
| Same function name | Same function signature |
| Different parameters | Derived class provides new implementation |
Usually same class |
Requires inheritance |
| Compile-time | Runtime when virtual |
| Return type alone cannot overload | override keyword recommended |
Overloading
void add(int a, int b);
void add(double a, double b);
Overriding
class Base {
public:
virtual void show() {}
};
class Derived : public Base {
public:
void show() override {}
};
Changing only the return type does not create a valid overload.
What is a virtual function?
A virtual function in C++ is a member function declared in a
base class using the virtual keyword, which is
designed to be overridden by a derived (child)
class.
Its primary purpose is to achieve runtime polymorphism.
Simple Example
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived" << endl;
}
};
int main() {
Derived d;
Base& b = d;
b.show();
}
Derived
Why does it print Derived?
Look carefully at this line:
Base& b = d;
Here:
- b is a Base reference
- The actual object is a Derived object
So we have:
Reference type Actual object
Base ---> Derived
When we call:
b.show();
because show() is virtual, C++ checks the actual object type at
runtime.
The actual object is Derived, so:
Derived::show()
is executed.
This is called dynamic dispatch or virtual
dispatch.
What happens without virtual?
This is extremely important for interviews.
Suppose we remove virtual:
class Base {
public:
void show() {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
void show() {
cout << "Derived" << endl;
}
};
int main() {
Derived d;
Base& b = d;
b.show();
}
Base
Why?
Because show() is not virtual.
The compiler uses the type of the reference:
Base& b
Therefore:
b.show();
calls:
Base::show()
So the difference is:
| Without virtual | With virtual |
|---|---|
| Compile-time binding | Runtime binding |
| Based on reference/pointer type | Based on actual object type |
| Static dispatch | Dynamic/virtual dispatch |
| Does not provide runtime polymorphism | Provides runtime polymorphism |
Why do we need Virtual Functions?
The most important use case is when we want to work with different derived objects through a
common base-class interface.
For example:
class Animal {
public:
virtual void sound() {
cout << "Animal sound" << endl;
}
};
class Dog : public Animal {
public:
void sound() override {
cout << "Bark" << endl;
}
};
class Cat : public Animal {
public:
void sound() override {
cout << "Meow" << endl;
}
};
Now:
int main() {
Dog dog;
Cat cat;
Animal* a1 = &dog;
Animal* a2 = &cat;
a1->sound();
a2->sound();
}
Bark
Meow
Notice something interesting:
Both pointers have the same type:
Animal*
But they point to different objects:
Animal* a1 ---> Dog
Animal* a2 ---> Cat
Because sound() is virtual, C++ chooses the correct implementation at
runtime.
This is runtime polymorphism.
Real-world analogy
Think about a common interface:
Payment | ├── CreditCard ├── UPI └── PayPal
You might have:
class Payment {
public:
virtual void pay() = 0;
};
Then:
class UPI : public Payment {
public:
void pay() override {
cout << "Paying using UPI";
}
};
class CreditCard : public Payment {
public:
void pay() override {
cout << "Paying using Credit Card";
}
};
Now your application can simply do:
Payment* payment;
and depending on the actual object:
Payment* ---> UPI
or:
Payment* ---> CreditCard
the appropriate pay() function is called.
That's one of the major benefits of polymorphism.
How does Virtual Function work internally?
Behind the scenes, the C++ compiler implements virtual functions using two
mechanisms:
VTABLE (Virtual Table):
VPTR (Virtual Pointer)
Consider:
class Base {
public:
virtual void show() {
cout << "Base";
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived";
}
};
Conceptually, the compiler creates something similar to:
Base ├── vptr └── other data Base vtable └── Base::show()
For Derived:
Derived ├── vptr └── other data Derived vtable └── Derived::show()
Let's understand each one.
What is a VTable?
VTable = Virtual Table
It is a compiler-generated table containing addresses/pointers to virtual functions.
For example, conceptually:
Base vtable ---------------- | Base::show() | ----------------
For Derived:
Derived vtable ------------------- | Derived::show() | -------------------
The actual implementation details are compiler-dependent, but this is a useful
mental model.
What is a VPTR?
VPTR = Virtual Pointer
An object of a class having virtual functions typically contains a
hidden pointer called a vptr.
The vptr points to the appropriate vtable.
Conceptually:
Derived object
+------------------+
| vptr ------------|------> Derived vtable
+------------------+ |
| other data | |
+------------------+ |
↓
Derived::show()
So when we have:
Derived d;
conceptually:
d │ │ vptr ↓ Derived vtable │ ↓ Derived::show()
How does this help with runtime polymorphism?
Consider:
Derived d;
Base* ptr = &d;
ptr->show();
At compile time, C++ knows:
ptr
is a Base*.
But because show() is virtual, the compiler generates code conceptually
equivalent to:
ptr ↓ object's vptr ↓ object's vtable ↓ correct show()
Since the actual object is Derived:
Base* ptr
|
↓
Derived object
|
↓
Derived vptr
|
↓
Derived vtable
|
↓
Derived::show()
Therefore:
ptr->show();
calls:
Derived::show();
Step-by-step execution
Suppose we have:
class Base {
public:
virtual void show() {
cout << "Base";
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived";
}
};
And:
Derived d;
Base* ptr = &d;
ptr->show();
Conceptually, the process is:
Step 1 — Create Derived object
Derived d;
The object has a hidden vptr.
d +----------------+ | vptr | +----------------+ | Derived data | +----------------+
Step 2 — vptr points to Derived's vtable
d.vptr | ↓ Derived vtable
The table contains something like:
Derived vtable +----------------------+ | Derived::show() | +----------------------+
Step 3 — Create base pointer
Base* ptr = &d;
The pointer points to the same Derived object:
ptr | ↓ +----------------+ | vptr ----------|-----> Derived vtable +----------------+ | | Derived data | ↓ +----------------+ Derived::show()
Step 4 — Call the function
ptr->show();
Because show() is virtual:
ptr ↓ vptr ↓ vtable ↓ Derived::show()
So output is:
Derived
Virtual Function vs Function Overriding
These concepts are related but not exactly the same.
Virtual function
The base class declares:
virtual void show();
This enables dynamic dispatch.
Overriding
The derived class provides its own implementation:
void show() override;
So:
virtual function
↓
enables
↓
overriding + runtime dispatch
What is a pure virtual function?
A pure virtual function in C++ is a virtual member
function that has no implementation inside the base class, defined
by adding = 0 to its declaration. It turns the base class into an
abstract class, which means you cannot create objects of that base
class directly.
Its main purpose is to create a strict rulebook or interface. It tells anyone writing a child
class: "You must provide your own version of this function, or
your code will not compile."
What is an abstract class?
An abstract class is a class that cannot be instantiated directly and
has at least one pure virtual function.
class Shape {
public:
virtual double area() = 0;
};
This is invalid:
Shape s; // ❌
But this is valid:
Shape* s = new Circle();
assuming Circle implements the pure virtual function.
Purpose:
Abstract classes are commonly used to define an interface or common contract for derived classes.
Can an abstract class have a constructor?
Yes. An abstract class can have a constructor, and that constructor is called when a
derived-class object is created.
class Animal {
public:
Animal() {
cout << "Animal constructor\n";
}
virtual void sound() = 0;
};
class Dog : public Animal {
public:
Dog() {
cout << "Dog constructor\n";
}
void sound() override {}
};
Creating:
Dog d;
results conceptually in:
Animal constructor
Dog constructor
Can an abstract class have data members?
Yes. An abstract class can have data members, constructors, normal member functions,
and pure virtual functions.
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
virtual void sound() = 0;
};
The fact that a class is abstract does not mean it cannot contain normal state.
Can we create a pointer to an abstract class?
Yes. We can create a pointer or reference to an abstract class, but we cannot create
an object of the abstract class itself.
class Shape {
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override {
cout << "Circle";
}
};
Shape* ptr = new Circle(); // ✅
ptr->draw();
But:
Shape s; // ❌
Can we call a virtual function from a constructor?
Yes, syntactically we can call a virtual function from a constructor, but
virtual dispatch does not call the derived-class override
while the base class is being constructed.
Example:
class Base {
public:
Base() {
show();
}
virtual void show() {
cout << "Base";
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived";
}
};
Derived d;
Base
Why?
During Base's constructor, the derived portion has not been constructed yet.
So the call behaves as a call to the current construction level.
Do not say:
"Calling a virtual function in a constructor calls the derived
implementation."
It does not.
Can we call a virtual function from a destructor?
Yes, but virtual dispatch is limited to the class whose destructor is
currently executing; it does not dispatch to a more-derived class that has already
been destroyed.
Example:
class Base {
public:
virtual ~Base() {
show();
}
virtual void show() {
cout << "Base";
}
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived destructor\n";
}
void show() override {
cout << "Derived";
}
};
When destroying a Derived object:
Derived destructor
Base destructor
Base::show()
not Derived::show().
Reason:
The derived portion has already been destroyed before the base destructor executes.
What is object slicing?
Object slicing occurs when a derived object is copied into a base object by value, causing the derived-specific part to be sliced off.
Example:
class Base {
public:
int x = 10;
};
class Derived : public Base {
public:
int y = 20;
};
Derived d;
Base b = d; // Object slicing
b contains only the Base portion.
The y member is not part of b.
How to avoid it?
Use a reference or pointer:
Base& b = d;
or:
Base* b = &d;
This is one reason polymorphic objects are commonly handled through references/pointers rather than copied by value.
What is the diamond problem?
The diamond problem occurs in multiple inheritance when a class inherits from two
classes that both inherit from the same base class, causing duplicate copies and
ambiguity.
Example:
class A {
public:
int x;
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
Structure:
A
/ \
B C
\ /
D
D contains two copies of A.
So:
D d;
// d.x = 10; // ❌ Ambiguous
The compiler doesn't know whether you mean:
d.B::x
or:
d.C::x
What is virtual inheritance?
Virtual inheritance ensures that only one shared base-class subobject exists when
multiple inheritance forms a diamond.
class A {
public:
int x;
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
Now:
A
/ \
virtual virtual
B C
\ /
D
D contains only one shared A subobject.
Therefore:
D d;
d.x = 10; // ✅
Virtual inheritance solves the duplicate base-subobject/ambiguity problem of the diamond.
It is different from a virtual function.
Composition vs aggregation
Both represent a "has-a" relationship, but composition implies strong ownership and aggregation represents a weaker relationship where the contained object can exist independently.
Composition
class Engine {};
class Car {
Engine engine;
};
The Car owns its Engine as part of its object state.
Conceptually:
Car └── Engine
If the Car object is destroyed, its Engine member is destroyed too.
Aggregation
class Teacher {};
class Department {
Teacher* teacher;
};
The teacher can exist independently of the department.
Composition → strong ownership Aggregation → weak/non-owning relationship
Composition vs inheritance
Inheritance represents an "is-a" relationship, while composition represents a "has-a" relationship.
Inheritance
class Dog : public Animal {};
A:
Dog IS-A Animal
Composition
class Car {
Engine engine;
};
A:
Car HAS-A Engine
Example:
Don't use inheritance just because two classes share some code.
Prefer:
class Car {
Engine engine;
};
if the relationship is naturally "has-a."
Why favor composition over inheritance?
Composition usually provides better flexibility, lower coupling, and better encapsulation than inheritance. It allows behavior to be changed by replacing or configuring contained objects rather than creating a rigid inheritance hierarchy.
Suppose:
class Car : public Engine {};
This says:
Car IS-A Engine
which is conceptually wrong.
Instead:
class Car {
Engine engine;
};
Now:
Car HAS-A Engine
Main advantages of composition
1. Lower coupling
Classes depend less on a rigid hierarchy.
2. Better flexibility
You can replace the component.
class Car {
unique_ptr<Engine> engine;
};
3. Better encapsulation
Internal implementation can be hidden.
4. Easier testing
You can inject a mock component.
5. Avoids inheritance problems
Such as:
- Fragile base-
classproblem - Unwanted inherited behavior
- Deep inheritance hierarchies
- Tight coupling
Pointers, Memory & Object Lifetime
What is a pointer?
A pointer is a variable that stores the memory address of another object.
int x = 10;
int* p = &x;
cout << p; // address of x
cout << *p; // 10
Here:
p → address of x *p → value stored at that address
NULL vs nullptr
NULL is a legacy null-pointer macro, commonly defined as 0 or 0L, while nullptr is a
dedicated C++11 null-pointer literal and is type-safe.
Example:
int* p1 = NULL;
int* p2 = nullptr;
Prefer:
int* p = nullptr;
Why?
Consider:
void fun(int);
void fun(int*);
fun(NULL);
Depending on how NULL is defined, overload resolution can become problematic.
But:
fun(nullptr);
clearly selects:
fun(int*);
What is a dangling pointer?
A dangling pointer is a pointer that refers to an object whose lifetime has ended.
Example:
int* p;
{
int x = 10;
p = &x;
}
// x no longer exists
cout << *p; // ❌ Undefined behavior
Another example:
int* p = new int(10);
delete p;
// p is dangling
Safer:
delete p;
p = nullptr;
What is a wild pointer?
A wild pointer is an uninitialized pointer that contains an indeterminate value and does not point to a known valid object.
int* p;
cout << *p; // ❌ Undefined behavior
Initialize pointers:
int* p = nullptr;
Difference
nullptr pointer → intentionally points to nothing
wild pointer → uninitialized/indeterminate pointer
dangling pointer → previously valid pointer whose target no longer exists
What is a void* pointer?
A void* is a pointer to an object of an unspecified type. It can hold the address of
different object types, but it must generally be converted to the appropriate pointer type
before dereferencing.
int x = 10;
void* p = &x;
int* ip = static_cast<int*>(p);
cout << *ip;
You cannot directly do:
void* p = &x;
// cout << *p; // ❌ Cannot dereference void*
Modern C++ generally prefers type-safe alternatives such as templates, rather than using
void*.
What is a function pointer?
A function pointer stores the address of a function and allows the function to be called indirectly.
Example:
int add(int a, int b) {
return a + b;
}
int (*ptr)(int, int) = add;
cout << ptr(2, 3); // 5
Here:
int (*ptr)(int, int)
means:
ptr is a pointer to a function taking two ints and returning an int.
What is a pointer to pointer?
A pointer to pointer stores the address of another pointer.
int x = 10;
int* p = &x;
int** pp = &p;
cout << **pp; // 10
Think:
x ↑ p ↑ pp
- pp → address of p
- *pp → p
- **pp → x
What is pointer arithmetic?
Pointer arithmetic allows moving a pointer across elements of an array or another contiguous sequence.
Example:
int arr[] = {10, 20, 30, 40};
int* p = arr;
cout << *p << '\n'; // 10
cout << *(p + 1) << '\n'; // 20
cout << *(p + 2) << '\n'; // 30
For an int*, p + 1 moves to the next int, not necessarily just one
byte.
Conceptually:
p + 1
↓
next int-sized element
Valid pointer arithmetic is mainly associated with elements of the same array/object sequence.
Common operations:
p++;
p--;
p + n;
p - n;
p1 - p2;
Pointer subtraction is meaningful when the pointers refer to elements of the same array.
What is a pointer to an array?
A pointer to an array points to the entire array object rather than directly to one element.
int arr[3] = {10, 20, 30};
int (*p)[3] = &arr;
cout << (*p)[0]; // 10
cout << (*p)[1]; // 20
Notice the parentheses:
int (*p)[3];
Without them:
int* p[3];
means:
array of 3 pointers.
This is a common interview syntax question.
Can we create an array of references?
No. C++ does not allow arrays of references.
Invalid:
int a = 10;
int b = 20;
int& arr[2] = {a, b}; // ❌
Instead, use:
int* arr[2] = {&a, &b};
Why can't we have an array of references?
A reference is an alias rather than a standalone object with its own reseatable storage semantics, while array elements must be actual objects/elements with well-defined storage and assignment behavior.
Stack vs heap
Stack
→ automatic/local objects → lifetime usually tied to scope → automatically cleaned up
Heap / Free Store
→ dynamically allocated objects → lifetime controlled by allocation/deallocation → preferably managed using RAII/smart pointers
What is Stack Memory?
The stack is memory typically used for function calls and objects with automatic storage duration.
Consider:
void fun() {
int x = 10;
int y = 20;
}
Conceptually:
Stack +----------------+ | y = 20 | +----------------+ | x = 10 | +----------------+
When fun() starts, the local variables are created.
When fun() ends, their lifetime ends automatically.
fun() starts
↓
x and y created
↓
fun() executes
↓
fun() ends
↓
x and y destroyed automatically
You don't write:
delete x;
because x wasn't dynamically allocated.
Why is it called "Stack"?
Function calls are typically managed using a call stack.
Suppose:
void A() {
B();
}
void B() {
C();
}
void C() {
int x = 10;
}
The call sequence is:
A() ↓ B() ↓ C()
Conceptually, the stack looks like:
+------------------+ | C() stack frame | | x = 10 | +------------------+ | B() stack frame | +------------------+ | A() stack frame | +------------------+
When C() finishes:
C() frame removed
Then B() finishes:
B() frame removed
This follows LIFO:
Last In, First Out.
That's why stack-based function-call management is called a stack.
Stack Variable Lifetime
Consider:
void fun() {
int x = 10;
if (true) {
int y = 20;
}
// y no longer exists here
}
x exists throughout the function:
fun() ┌──────────────────────┐ │ x = 10 │ │ │ │ if block │ │ ┌──────────────┐ │ │ │ y = 20 │ │ │ └──────────────┘ │ │ │ └──────────────────────┘
y has a shorter lifetime because its scope is the if block.
When the block ends:
if (true) {
int y = 20;
} // y's lifetime ends here
y is destroyed automatically.
What is Heap Memory?
The heap, more precisely the free store in C++ terminology, is used for dynamic allocation.
For example:
int* p = new int(20);
Here:
p ↓ +----------------+ | 20 | +----------------+ dynamic storage
p itself is a local variable, while the int created by new has dynamic storage
duration.
Conceptually:
Stack Heap / Free Store +----------+ +----------+ | p | ----------> | 20 | +----------+ +----------+
This distinction is extremely important.
p and 20 are not the same thing.
int* p = new int(20);
- p → pointer variable with automatic storage if declared locally
- *p → dynamically allocated
int
Why do we need Dynamic Memory?
Suppose you don't know at compile time how many objects you need.
For example:
int n;
cin >> n;
int* arr = new int[n];
Here n is determined at runtime.
The dynamically allocated array has a lifetime independent of the local pointer variable.
However, in modern C++, you generally shouldn't write this manually. Prefer:
std::vector<int> arr(n);
The vector manages its dynamic memory automatically.
Stack vs Heap Example
Consider:
void fun() {
int x = 10;
int* p = new int(20);
delete p;
}
Conceptually:
STACK HEAP / FREE STORE
+-----------+ +-----------+ | x = 10 | | 20 | +-----------+ +-----------+ | p | --------------> | | +-----------+ +-----------+
p stores the address of the dynamically allocated int.
When:
delete p;
is executed:
Heap object destroyed
↓
memory returned to allocator
Then p itself is destroyed automatically when fun() ends.
What happens if you forget delete?
Consider:
void fun() {
int* p = new int(20);
}
When fun() ends:
p disappears
But:
20
still exists in dynamically allocated memory.
You have lost the pointer needed to access it.
This is called a memory leak.
Stack Heap
p ────────────────X 20
↑
no pointer
Therefore, manual dynamic allocation can be dangerous.
Modern C++: Use RAII
Instead of:
int* p = new int(20);
delete p;
prefer:
auto p = std::make_unique<int>(20);
Now:
Stack Dynamic storage p ─────────────────────> 20
When p goes out of scope:
p destroyed ↓ unique_ptr automatically releases ↓ dynamic object destroyed
No explicit delete is required.
This is called RAII — Resource Acquisition Is Initialization.
Static memory vs stack vs heap
A useful interview-level model is:
| Storage | Typical use | Lifetime |
|---|---|---|
| Static storage | Global/static variables |
Entire program |
| Automatic storage | Local variables | Scope/lifetime of object |
| Dynamic storage | new, containers internally | Until released |
Example:
int global = 10; // static storage duration
void fun() {
int x = 20; // automatic storage duration
static int y = 30; // static storage duration
int* p = new int(40); // dynamic storage
delete p;
}
What happens when new is used?
new allocates storage and then constructs an object in that storage, returning a pointer to the object.
int* p = new int(10);
Conceptually:
1. Allocate storage
2. Construct/initialize int with 10
3. Return its address
For a class:
Student* s = new Student("Dilip");
new allocates storage and invokes the appropriate constructor.
What happens when delete is used?
delete destroys the object and then releases the dynamically allocated storage obtained from the corresponding scalar new.
Student* p = new Student();
delete p;
Conceptually:
delete p ↓ destructor ↓ release allocated storage
After deletion:
p = nullptr;
is often useful to avoid accidentally keeping a dangling pointer.
new vs malloc
| new | malloc |
|---|---|
| C++ operator | C library function |
| Returns typed pointer | Returns void* |
| Calls constructors | Does not call constructors |
Throws std::bad_alloc by default on failure |
Returns nullptr on failure |
| Use delete | Use free |
| Supports initialization | Raw allocation only |
Example:
Student* s = new Student();
delete s;
versus:
Student* s = static_cast<Student*>(malloc(sizeof(Student)));
free(s);
The second does not construct a proper Student object.
In modern C++, generally prefer containers and smart pointers over manual new.
delete vs free
delete is the C++ counterpart for objects allocated with new; free releases memory allocated by malloc/calloc/realloc.
Correct pairs:
new → delete new[] → delete[] malloc → free calloc → free realloc → free
Do not mix them:
int* p = new int;
free(p); // ❌
int* q = (int*)malloc(sizeof(int));
delete q; // ❌
Mixing allocation/deallocation mechanisms can cause undefined behavior.
new[] vs delete[]
For an array:
int* arr = new int[5];
delete[] arr;
The matching forms are:
new → delete new[] → delete[]
For class objects, delete[] also ensures the destructors of the array elements are
called appropriately.
What happens if delete is used instead of delete[]?
Example:
int* arr = new int[5];
delete arr; // ❌
Using delete on memory allocated with new[] results in undefined behavior.
Correct:
delete[] arr;
Similarly:
int* p = new int;
delete[] p; // ❌
is also undefined behavior.
What is a memory leak?
A memory leak occurs when dynamically allocated storage remains allocated but the program loses the ability to release it.
Example:
void fun() {
int* p = new int(10);
// Forgot delete
}
When fun() ends, p disappears, but the dynamically allocated memory remains
inaccessible.
Repeated leaks can cause:
- Increasing memory usage
- Performance degradation
- Eventually allocation failure
Modern C++ solution
Prefer RAII:
auto p = std::make_unique<int>(10);
Now the memory is automatically released.
What is double deletion?
Double deletion occurs when the same dynamically allocated object is deallocated more than once, causing undefined behavior.
int* p = new int(10);
delete p;
delete p; // ❌
Safer:
delete p;
p = nullptr;
Then:
delete p; // delete nullptr is safe
But setting pointers to nullptr does not solve ownership problems by itself. Smart
pointers are usually better.
What is use-after-free?
Use-after-free occurs when a program accesses an object after its storage has already been released.
int* p = new int(10);
delete p;
cout << *p; // ❌
The pointer still contains an address, but the object's lifetime has ended.
This is a serious source of:
- Crashes
- Security vulnerabilities
- Undefined behavior
What is memory corruption?
Memory corruption occurs when a program incorrectly reads from or writes to memory, damaging data or memory-management structures.
Example:
int arr[3];
arr[5] = 100; // ❌ Out-of-bounds write
Other causes:
- Buffer overflow
- Use-after-free
- Double free
- Invalid pointer arithmetic
- Writing through an invalid pointer
What is undefined behavior?
Undefined behavior means the C++ standard imposes no requirements on what happens when the program performs an operation classified as undefined behavior.
Examples:
int* p = nullptr;
*p = 10; // UB
int arr[3];
arr[10] = 5; // UB
int* p = new int;
delete p;
*p = 10; // UB
The compiler does not have to:
- Throw an exception
- Crash
- Print an error
- Produce consistent behavior
Undefined behavior is not the same as "it will definitely crash."
What is object lifetime?
Object lifetime is the period during which an object has been created and exists as a valid C++ object until its lifetime ends.
For:
void fun() {
Student s;
}
Conceptually:
s constructor
↓
object lifetime begins
↓
use object
↓
scope ends
↓
destructor
↓
object lifetime ends
For dynamically allocated objects:
Student* p = new Student();
delete p;
The object's lifetime ends when it is destroyed by delete.
Storage and object lifetime are related but not identical concepts.
What is dynamic memory allocation?
Dynamic memory allocation means obtaining storage during program execution rather than having the object's storage duration determined automatically.
Traditional C++:
int* p = new int(10);
delete p;
Modern C++:
auto p = make_unique<int>(10);
Containers also dynamically allocate internally:
vector<int> v;
v.push_back(10);
v.push_back(20);
You normally don't manually manage that memory.
What is placement new?
Placement new constructs an object at a memory address that has already been allocated.
Example:
#include <new>
char buffer[sizeof(int)];
int* p = new (buffer) int(42);
cout << *p;
Here:
buffer → already-existing storage
↓
placement new
↓
construct int there
Unlike normal new, placement new does not allocate memory.
It only constructs an object in the supplied storage.
What is memory alignment?
Memory alignment is the requirement that objects of certain types be stored at addresses suitable for that type.
For example, a type may require alignment to a 4-byte or 8-byte boundary.
You can inspect alignment with:
alignof(int)
and request aligned storage using:
alignas(64) int x;
Alignment can improve:
- Correctness
- CPU access efficiency
- SIMD/cache-line-oriented designs
Misaligned access can be problematic depending on the type and platform.
What is structure padding?
Structure padding is extra unused bytes inserted between or after members to satisfy alignment requirements.
Example:
struct A {
char c;
int x;
};
You might expect:
1 + 4 = 5 bytes
But the actual size may be:
8 bytes
Conceptually:
char c ↓ [c][padding][padding][padding] [int x][int x][int x][int x]
The exact layout is implementation-dependent.
Why can sizeof(struct) be larger than the sum of its members?
Because the compiler may insert padding between members and at the end of the structure to satisfy alignment requirements.
Example:
struct A {
char c;
int x;
};
Possible layout:
1 byte → char 3 bytes → padding 4 bytes → int ---------------- 8 bytes
The trailing padding can also be necessary so that elements of:
A arr[10];
have correctly aligned members.
Compare:
struct A {
char c;
int x;
char d;
};
with:
struct B {
int x;
char c;
char d;
};
Reordering members can sometimes reduce padding.
Copy Semantics, Move Semantics & RAII
What is an lvalue?
An lvalue is a value category for an expression that identifies an object or function with a persistent identity and can generally appear on the left side of assignment when it is modifiable.
Example:
int x = 10;
x = 20;
x is an lvalue.
Another example:
int* p = &x;
x has an identifiable location.
What is an rvalue?
An rvalue is an expression that is generally associated with a temporary value or an object whose resources can potentially be moved from.
Example:
int x = 10;
int y = x + 5;
x + 5 is an rvalue.
Also:
int y = 10;
10 is a prvalue, which is a category of rvalue.
Simple interview intuition
lvalue → has identity
rvalue → temporary/value expression
This intuition is useful, although the complete C++ value-category system is more precise.
What is an rvalue?
An rvalue is an expression that is generally associated with a temporary value or an object whose resources can potentially be moved from.
Example:
int x = 10;
int y = x + 5;
x + 5 is an rvalue.
Also:
int y = 10;
10 is a prvalue, which is a category of rvalue.
Simple interview intuition
lvalue → has identity
rvalue → temporary/value expression
This intuition is useful, although the complete C++ value-category system is more precise.
What is a prvalue?
A prvalue is a "pure rvalue" that primarily represents a value or a temporary result used to initialize or compute another object.
Examples:
10
x + y
string("Hello")
For example:
int x = 10;
10 is a prvalue.
Relationship
Expressions
├── lvalue
└── rvalue
├── prvalue
└── xvalue
This distinction becomes important when discussing move semantics and overload resolution.
What is an lvalue reference?
An lvalue reference, written as T&, normally binds to an lvalue.
int x = 10;
int& ref = x;
Now:
ref = 20;
cout << x; // 20
It is an alias for x.
Normally:
int& ref = 10; // ❌
because a non-const lvalue reference cannot bind to that temporary.
But:
const int& ref = 10; // ✅
is allowed.
What is an rvalue reference?
An rvalue reference, written as T&&, can bind to rvalues and is primarily used for move semantics and perfect forwarding.
Example:
int&& ref = 10;
And:
string s = "Hello";
string&& ref = std::move(s);
A common use is a move constructor:
class Buffer {
public:
Buffer(Buffer&& other) {
// Transfer resources
}
};
Why do we need rvalue references?
Because they allow C++ to distinguish between:
existing object → potentially copy
temporary/expiring object → potentially move
This is the foundation of efficient move semantics.
What is a copy constructor?
A copy constructor creates a new object by initializing it from another object of the same
class.
Typical syntax:
class Student {
public:
Student(const Student& other) {
// copy data
}
};
Example:
Student s1;
Student s2 = s1;
Here s2 is a new object, so the copy constructor is used.
Student s2;
s2 = s1;
This is copy assignment, not copy construction.
What is copy assignment?
Copy assignment copies the state of an existing object into another already-existing object.
Example:
Student s1;
Student s2;
s2 = s1;
This invokes:
operator=
Conceptually:
Student& operator=(const Student& other);
Copy constructor vs copy assignment
This is a very common interview question.
| Copy Constructor | Copy Assignment |
|---|---|
| Creates a new object | Object already exists |
| Initializes object | Replaces existing state |
| Student b = a | b = a |
Student(const Student&) |
operator=(const Student&) |
Example:
Student a;
Student b = a; // Copy constructor
Student c;
c = a; // Copy assignment
New object → constructor. Existing object → assignment.
When is a copy constructor called?
Common situations:
1. Initializing from another object
Student a;
Student b = a;
2. Passing by value
void fun(Student s);
Student a;
fun(a);
A copy may be made for the parameter.
3. Returning by value
Student create() {
Student s;
return s;
}
A copy may conceptually be involved, but modern C++ often eliminates it through copy elision/NRVO.
4. Explicit construction
Student b(a);
What is shallow copy?
Shallow copy copies member values as they are, including pointer values, so two objects may end up pointing to the same resource.
A shallow copy creates a new object that is a copy of an existing object, but does not allocate new memory for any dynamically allocated members. Instead, it copies the values of the members directly. If a member is a pointer, the shallow copy will copy the pointer value (address) rather than the data it points to.
Example:
class Buffer {
public:
int* data;
Buffer(int value) {
data = new int(value);
}
};
Suppose:
Buffer a(10);
Buffer b = a;
With a shallow copy:
a.data ──┐
↓
[10]
↑
b.data ──┘
Both pointers point to the same memory.
What is deep copy?
Deep copy creates an independent copy of the resource instead of copying only the pointer.
Example:
class Buffer {
int* data;
public:
Buffer(int value) {
data = new int(value);
}
Buffer(const Buffer& other) {
data = new int(*other.data);
}
~Buffer() {
delete data;
}
};
Now:
Buffer a(10);
Buffer b = a;
Conceptually:
a.data → [10]
b.data → [10]
They are two separate allocations.
Why is shallow copy dangerous with raw pointers?
Shallow copying an owning raw pointer can cause multiple objects to own the same resource,
leading to double deletion, dangling pointers, and unexpected modifications.
Example:
class Buffer {
public:
int* data;
Buffer(int value) {
data = new int(value);
}
~Buffer() {
delete data;
}
};
If the compiler-generated copy simply copies the pointer:
Buffer a(10);
Buffer b = a;
then:
a.data ──┐
↓
[10]
↑
b.data ──┘
When b and a are destroyed, both may try to delete the same memory.
When the objects are destroyed, both destructors execute:
b destroyed ↓ delete b.data ↓ memory freed a destroyed ↓ delete a.data ↓ same memory is freed again
This is called double deletion / double free, leading
to undefined behavior.
How do you implement deep copy?
You need to allocate independent storage and copy the value.
class Buffer {
int* data;
public:
Buffer(int value) {
data = new int(value);
}
Buffer(const Buffer& other) {
data = new int(*other.data);
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
*data = *other.data;
}
return *this;
}
~Buffer() {
delete data;
}
};
Important components:
copy constructor
copy assignment
destructor
This is directly related to the Rule of 3.
What Happens If You Don't Define a Copy Constructor?
This is where the compiler-generated copy constructor becomes important.
If you don't define a copy constructor, C++ can implicitly declare one for you.
For example:
class Buffer {
public:
int* data;
Buffer(int value) {
data = new int(value);
}
~Buffer() {
delete data;
}
};
You didn't write :
Buffer(const Buffer& other)
But C++ can provide an implicitly-declared copy constructor performs member-wise copying.
Conceptually, it behaves approximately like:
Buffer(const Buffer& other) {
data = other.data;
}
Notice:
data = other.data;
It copies the pointer value.
It does NOT do:
data = new int(*other.data);
Therefore:
Buffer a(10);
Buffer b = a;
results conceptually in:
a.data ───────┐
↓
[10]
↑
b.data ───────┘
What Happens If You Don't Define Copy Assignment?
This is the second question you asked.
Suppose:
class Buffer {
public:
int* data;
Buffer(int value) {
data = new int(value);
}
~Buffer() {
delete data;
}
};
Now:
Buffer a(10);
Buffer b(20);
b = a;
You didn't define:
Buffer& operator=(const Buffer& other)
So C++ can provide an implicitly-declared copy-assignment operator that performs member-wise assignment.
Conceptually:
b.data = a.data;
Before assignment:
a.data → [10]
b.data → [20]
After:
a.data ─────┐
↓
[10]
↑
b.data ─────┘
Now both point to the same resource.
But there is an additional problem.
The original:
[20]
that b.data pointed to is now lost.
Before:
b.data → [20]
After:
b.data → [10]
[20] has no owner/pointer
That's a memory leak.
Then when both destructors execute, they can attempt to delete the same [10] resource.
Again, undefined behavior.
Correct Copy Assignment for Deep Copy
You need to implement:
Buffer& operator=(const Buffer& other)
For example:
class Buffer {
private:
int* data;
public:
Buffer(int value) {
data = new int(value);
}
Buffer(const Buffer& other) {
data = new int(*other.data);
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
~Buffer() {
delete data;
}
};
Now:
Buffer a(10);
Buffer b(20);
b = a;
produces:
Before:
a.data → [10]
b.data → [20]
Assignment:
delete [20]
↓
allocate new memory
↓
copy 10
After:
a.data → [10]
b.data → [10]
But these are separate allocations.
Why if (this != &other)?
Consider:
a = a;
This is called self-assignment.
If our assignment operator blindly did:
delete data;
data = new int(*other.data);
then after:
delete data;
both:
this->data
other.data
would refer to the same freed memory because this == &other.
Then:
*other.data
would access freed memory.
That's undefined behavior.
Therefore:
if (this != &other)
protects against self-assignment.
What is the Rule of 3?
If a class manually manages a resource and defines one of these:
Destructor
Copy constructor
Copy assignment operator
you often need to consider all three.
This is called the Rule of Three.
For our class:
class Buffer {
public:
Buffer(int value);
Buffer(const Buffer& other); // 1
Buffer& operator=(const Buffer& other); // 2
~Buffer(); // 3
};
Why?
Because all three are related to resource ownership.
If your class owns:
int* data;
then you need to carefully define what happens when:
- an object is copied
- one object is assigned to another
- an object is destroyed
What is the Rule of 5?
The Rule of 5 extends the Rule of 3 to include move constructor and move assignment operator.
The five special member functions are:
1. Destructor
2. Copy constructor
3. Copy assignment
4. Move constructor
5. Move assignment
Example:
class Buffer {
public:
Buffer(const Buffer&);
Buffer& operator=(const Buffer&);
Buffer(Buffer&&) noexcept;
Buffer& operator=(Buffer&&) noexcept;
~Buffer();
};
Copy constructor in Rule of 5
The copy constructor is:
Buffer(const Buffer& other);
It creates a new object by copying another object.
Example:
Buffer b = a;
Copy assignment operator in Rule of 5
The copy assignment operator is:
Buffer& operator=(const Buffer& other);
Example:
b = a;
The object b already exists.
Destructor in Rule of 5
The destructor:
~Buffer();
releases resources owned by the object.
For raw memory:
~Buffer() {
delete[] data;
}
With RAII types like vector and unique_ptr, you usually don't need to write one
yourself.
What is the Rule of 0?
The Rule of 0 says that classes should preferably avoid manually managing resources so that they don't need to define these special member functions themselves.
Instead of:
class Buffer {
int* data;
};
prefer:
class Buffer {
vector<int> data;
};
or:
class Buffer {
unique_ptr<int[]> data;
};
Now the resource-owning class handles its own lifetime.
Best modern C++ principle:
Prefer the Rule of 0 whenever possible.
Move constructor
A move constructor initializes a new object by transferring resources from another object that is eligible to be moved from.
Buffer(Buffer&& other) noexcept;
Example:
Buffer a;
Buffer b = std::move(a);
Instead of copying a large resource, b can take ownership of it.
Move assignment operator
Move assignment transfers resources from an existing source object into an already-existing destination object.
Buffer& operator=(Buffer&& other) noexcept;
Example:
Buffer a;
Buffer b;
b = std::move(a);
Difference:
Move constructor
→ creates new object
Move assignment
→ existing object receives resource
What are move semantics?
Move semantics allow resources to be transferred from one object to another instead of being copied.
Example:
vector<int> a(1000000);
vector<int> b = std::move(a);
Instead of copying one million elements, the implementation can transfer ownership of the underlying buffer.
Conceptually:
Before:
a → [large buffer] b → nothing
After move:
a → valid but moved-from state b → [large buffer]
Why were move semantics introduced?
Move semantics were introduced to avoid expensive copies of temporary or expiring objects and make resource transfer efficient.
Before move semantics, returning or transferring large resource-owning objects could require expensive copies.
Modern C++ can instead transfer resources.
This is especially useful for:
- vector
- string
- File/resource handles
- Large buffers
- Smart pointers
What is an rvalue reference?
An rvalue reference is written as T&& and can bind to rvalues. It enables move semantics and perfect forwarding.
string&& ref = string("Hello");
Also:
string s = "Hello";
string&& ref = std::move(s);
Remember:
T& → lvalue reference T&& → rvalue reference
What does std::move() actually do?
std::move() does not move an object by itself. It casts an expression into an
xvalue, allowing move constructors or move assignment operators to be selected.
Example:
string a = "Hello";
string b = std::move(a);
The important part is:
std::move(a)
which enables b's move constructor to be considered.
Copy constructor vs Move constructor
| Copy | Move |
|---|---|
const T& |
T&& |
| Duplicates resources | Transfers resources |
| Usually more expensive | Often cheaper |
| Source remains unchanged | Source becomes moved-from |
Example:
Buffer b = a; // copy
Buffer c = std::move(a); // move
Copy assignment vs Move assignment
Copy assignment
b = a;
Conceptually:
duplicate a's resources
Move assignment
b = std::move(a);
Conceptually:
release b's old resource
take a's resource
leave a valid
Typical signature:
Buffer& operator=(Buffer&& other) noexcept;
Why should move constructors often be noexcept?
noexcept is a C++ exception specifier that promises a function will
not throw any exceptions. [1]
A move constructor is often marked noexcept because standard containers such as
std::vector may prefer moving elements during reallocation only when the move
operation is known not to throw.
Example:
class Buffer {
public:
Buffer(Buffer&& other) noexcept {
// transfer resources
}
};
Consider vector growth:
vector<Buffer> v;
v.push_back(Buffer());
When the vector needs to reallocate, it wants a safe and efficient way to transfer existing elements.
If moving could throw, the vector may choose copying instead when a suitable copy operation exists, to preserve exception-safety guarantees.
What happens to an object after it is moved from?
A moved-from object remains a valid object, but its value is generally unspecified unless the type documents a stronger post-move state.
Example:
string a = "Hello";
string b = std::move(a);
Afterward, a is still a valid std::string, but its exact contents should generally
not be relied upon unless specified by the type.
For many standard library types, the object remains in a valid but unspecified state.
Can a moved-from object be used?
Yes. A moved-from object can generally be destroyed, assigned a new value, and used in operations permitted by its type's documented post-move state. You should not assume its old value remains.
Example:
string a = "Hello";
string b = std::move(a);
a = "New value"; // ✅
cout << a; // New value
Avoid assuming:
cout << a;
will necessarily print the old "Hello".
Valid but unspecified state.
Why does vector prefer moving during reallocation when possible?
Suppose:
vector<Buffer> v;
When capacity is exhausted, the vector may need to allocate a larger buffer and transfer its elements.
Conceptually:
Old memory
[ A ][ B ][ C ]
↓ reallocation
New memory
[ A ][ B ][ C ]
Moving can transfer resources instead of duplicating them.
But vector also has to preserve exception-safety guarantees. Therefore, if moving isn't
noexcept and copying is available, it may prefer copying.
vector uses move construction when it is safe and advantageous, particularly when the move
constructor is noexcept; otherwise it may copy to maintain exception
guarantees.
Copy Elision, RVO and NRVO in C++
These three terms are closely related, so the easiest way to understand them is:
Copy Elision
│
├── RVO
│
└── NRVO
Copy elision is the general concept.
RVO and NRVO are specific situations where copy/move construction can be avoided.
The key idea is:
Instead of creating one object and then copying/moving it into another object, C++ can construct the final object directly where it is needed.
First understand the problem
Consider:
class A {
public:
A() {
cout << "Constructor\n";
}
A(const A&) {
cout << "Copy constructor\n";
}
A(A&&) {
cout << "Move constructor\n";
}
};
Now:
A create() {
return A();
}
int main() {
A obj = create();
}
You might imagine that several objects are involved:
A() ↓ temporary object ↓ copy/move ↓ obj
But C++ can avoid those unnecessary intermediate objects.
Instead:
A() ↓ obj
The object is constructed directly as obj.
That's the basic idea behind copy elision.
What exactly is Copy Elision?
Copy elision means:
C++ omits an otherwise unnecessary copy or move construction of an object.
Normally, you might expect:
Object A ↓ copy/move ↓ Object B
But with copy elision:
Object A/B ↓ directly constructed
So instead of:
create object
↓
copy it
↓
destroy original
C++ can do:
construct final object directly
This can improve performance because it avoids:
- unnecessary object construction
- copy construction
- move construction
- destruction of temporary objects
What is RVO?
RVO = Return Value Optimization
It is a specific form of copy elision related to returning a temporary/prvalue object from a function.
Consider:
A create() {
return A();
}
And:
A obj = create();
The important line is:
return A();
A() creates a temporary/prvalue.
You might conceptually imagine:
create() A() ↓ temporary A ↓ copy/move ↓ obj
But with RVO:
create() A() ↓ obj
The A is constructed directly in the storage associated with obj.
Why is it called Return Value Optimization?
Because the optimization happens around a function's return value.
Example:
A create() {
return A();
}
The function is returning an A.
Instead of:
create A ↓ move/copy A ↓ caller
C++ can construct the return object directly in the caller's destination.
So:
RVO Function │ │ return A() ↓ Caller │ ↓ obj
Now What is NRVO?
NRVO = Named Return Value Optimization
The important word is:
Named
Consider:
A create() {
A obj;
return obj;
}
Here:
A obj;
creates a named local object.
Then:
return obj;
returns that named object.
Without NRVO, conceptually:
create() local obj ↓ copy/move ↓ returned object ↓ caller
With NRVO:
create() obj ↓ constructed directly ↓ caller object
So the copy/move can be eliminated.
Why is it called "Named" RVO?
Compare these two:
RVO
A create() {
return A();
}
The returned object is a temporary/prvalue.
NRVO
A create() {
A obj;
return obj;
}
The returned object is a named local variable called obj.
That's where the distinction comes from:
RVO
→ return temporary/prvalue
NRVO
→ return named local object
What is RAII?
RAII stands for Resource Acquisition Is Initialization. It ties resource ownership to an object's lifetime: the resource is acquired during initialization and released automatically when the object is destroyed.
Example:
class File {
public:
File() {
// acquire file resource
}
~File() {
// release file resource
}
};
Usage:
void process() {
File file;
// use file
} // destructor automatically called
The same principle applies to:
- Memory
- Files
- Mutexes
- Sockets
- Database connections
- Locks
Why is RAII important?
RAII makes resource management automatic and exception-safe by tying cleanup to object lifetime.
Without RAII:
resource = acquire();
if (error)
return; // Could forget cleanup
release(resource);
With RAII:
Resource r;
When r goes out of scope, its destructor automatically releases the resource.
Main benefits
- Automatic cleanup
- Exception safety
- Less manual resource management
- Clear ownership
- Fewer leaks
How does RAII prevent memory leaks?
Instead of:
int* p = new int(10);
and remembering:
delete p;
use:
auto p = make_unique<int>(10);
When p goes out of scope:
scope ends ↓ unique_ptr destructor ↓ delete resource
Therefore, even if an exception occurs, cleanup still happens during stack unwinding.
RAII and exception safety
Consider:
void process() {
auto p = make_unique<int>(10);
throw runtime_error("Error");
}
Even though the function throws:
exception ↓ stack unwinding ↓ p destructor ↓ memory released
This is a major reason RAII is fundamental to C++.
RAII ensures resources are released automatically during both normal scope exit and exception-driven stack unwinding.
RAII and mutexes
Instead of manually:
mutex.lock();
// code
mutex.unlock();
use:
lock_guard<mutex> lock(mutex);
When lock goes out of scope:
scope ends ↓ lock_guard destructor ↓ mutex unlocked
Example:
mutex m;
int counter = 0;
void increment() {
lock_guard<mutex> lock(m);
counter++;
}
Even if an exception occurs, the mutex is released.
This is RAII.
RAII and file handling
A file wrapper can acquire a file in its constructor and close it in its destructor.
In modern C++, standard library types already apply this idea.
For example:
#include <fstream>
void readFile() {
ifstream file("data.txt");
// use file
} // file automatically closes
You don't normally need:
file.close();
just to ensure cleanup when the object goes out of scope.
RAII ties the file resource to the lifetime of the stream object, so the file is automatically closed when the stream is destroyed.
Smart Pointers & Modern Memory Management
What are smart pointers?
Smart pointers are C++ objects that manage dynamically allocated resources automatically and release them when they are no longer needed.
Simple explanation
A raw pointer:
int* p = new int(10);
delete p;
If we forget delete, we can get a memory leak.
A smart pointer manages the lifetime automatically:
auto p = std::make_unique<int>(10);
When p goes out of scope, the memory is automatically released.
The main smart pointers are:
| Smart pointer | Ownership |
|---|---|
unique_ptr |
Single owner |
shared_ptr |
Multiple owners |
weak_ptr |
Non-owning observer |
Code
#include <memory>
int main() {
std::unique_ptr<int> p = std::make_unique<int>(10);
}
No explicit delete is required.
Why is automatic cleanup possible?
Because smart pointers use RAII. Their destructor releases the owned resource.
Don't say:
Smart pointers eliminate all memory leaks.
They greatly reduce ownership-related leaks, but incorrect ownership design can still cause
leaks, especially with shared_ptr cycles.
Why should we prefer smart pointers over raw owning pointers?
Smart pointers provide automatic ownership and lifetime management, reducing memory leaks,
double deletion, and exception-safety problems.
Simple explanation
Raw owning pointer:
Resource* r = new Resource();
doSomething();
delete r;
If doSomething() throws an exception, delete r might never execute.
With RAII:
auto r = std::make_unique<Resource>();
doSomething();
The destructor of unique_ptr runs during stack unwinding.
Main advantages
- Automatic cleanup
- Clear ownership
- Exception safety
- Prevents many
double-delete errors - Easier resource management
- Expresses ownership in the type
Raw pointers are not bad.
The important distinction is:
Prefer smart pointers for ownership; raw pointers are often fine for non-owning observation.
What is unique_ptr?
unique_ptr is a smart pointer that provides exclusive ownership of a dynamically
allocated object.
Only one unique_ptr owns the object at a time.
Code
#include <memory>
int main() {
std::unique_ptr<int> p = std::make_unique<int>(10);
std::cout << *p;
}
When p goes out of scope:
p destroyed ↓ owned object deleted
unique_ptr cannot be copied.
std::unique_ptr<int> p1 = std::make_unique<int>(10);
// std::unique_ptr<int> p2 = p1; // ERROR
But it can be moved.
std::unique_ptr<int> p2 = std::move(p1);
Now:
p1 → nullptr
p2 → object
Don't say unique_ptr means the object can never change ownership.
Ownership can be transferred using move semantics.
What is shared_ptr?
shared_ptr provides shared ownership of an object using reference counting.
Multiple shared_ptrs can own the same object.
Code
#include <memory>
auto p1 = std::make_shared<int>(10);
auto p2 = p1;
auto p3 = p1;
Conceptually:
┌──────────┐
p1 ───►│ │
p2 ───►│ 10 │
p3 ───►│ │
└──────────┘
use_count = 3
When one owner disappears:
use_count = 2
When the last owner disappears:
object destroyed
shared_ptr does not mean "thread-safe object."
Multiple threads can safely manipulate separate shared_ptr instances that share
ownership under the standard's atomic-control-block guarantees, but the managed
object's data is not automatically thread-safe.
What is weak_ptr?
weak_ptr is a non-owning smart pointer that observes an object managed by
shared_ptr without increasing its strong reference count.
Code
auto shared = std::make_shared<int>(10);
std::weak_ptr<int> weak = shared;
The weak_ptr observes the object but does not own it.
shared_ptr ───► object weak_ptr ───► object
But only the shared_ptr contributes to the strong ownership count.
Why useful?
Most importantly:
weak_ptr helps break shared_ptr ownership cycles.
unique_ptr vs shared_ptr
| Feature | unique_ptr | shared_ptr |
|---|---|---|
| Ownership | Exclusive | Shared |
| Copyable | No | Yes |
| Movable | Yes | Yes |
| Reference counting | No | Yes |
| Overhead | Low | Higher |
| Default choice | Usually preferred | When shared ownership is required |
I prefer unique_ptr by default because it has clear exclusive ownership and
lower overhead. I use shared_ptr only when multiple independent owners
genuinely need shared lifetime.
Don't choose shared_ptr just because multiple parts of the program need
access.
If they don't own the object, use a reference or raw pointer for observation.
shared_ptr vs weak_ptr
shared_ptr owns the object and increases the strong reference count, while
weak_ptr only observes the object and does not keep it alive.
Example:
auto p = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p;
std::weak_ptr<int> w = p;
Conceptually:
shared ownership count = 2
weak observer count = 1
Destroying p:
strong count = 1
Destroying p2:
strong count = 0
object destroyed
The weak_ptr does not prevent this.
Why can't unique_ptr be copied?
Because copying would create two owners of the same resource, violating exclusive ownership.
Suppose copying were allowed:
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = p1;
Then:
p1 ──┐
├──► same object
p2 ──┘
Both would try to delete the same object.
That could cause double deletion.
Therefore:
unique_ptr(const unique_ptr&) = delete;
Conceptually.
How do you transfer ownership of unique_ptr?
Ownership is transferred using move semantics with std::move().
auto p1 = std::make_unique<int>(10);
auto p2 = std::move(p1);
After this:
p1 → nullptr p2 → 10
std::move() itself doesn't move the object.
It enables the move constructor/assignment to be selected.
What does std::move(unique_ptr) do?
It converts the unique_ptr into an rvalue so its move constructor or move assignment
can transfer ownership.
auto p1 = std::make_unique<int>(10);
auto p2 = std::move(p1);
The ownership is transferred by the move operation, not by
std::move() itself.
Wrong:
std::move() moves the object.
Correct:
std::move() is essentially a cast that enables moving.
How does shared_ptr reference counting work?
shared_ptr maintains a strong reference count representing how many
shared_ptrs own the object. When the count reaches zero, the managed object is
destroyed.
Example:
auto p1 = std::make_shared<int>(10);
auto p2 = p1;
auto p3 = p1;
Conceptually:
p1 ──┐ p2 ──┼──► object p3 ──┘ strong count = 3
Then:
p2.reset();
Count becomes:
2
Then:
p3.reset();
Count:
1
Finally:
p1.reset();
Count:
0
Object gets destroyed.
What is a control block?
A control block is an internal structure associated with a shared_ptr that stores
ownership-related information such as reference counts and usually the deleter.
Conceptually:
Control Block
┌─────────────────┐
│ strong count │
│ weak count │
│ deleter │
│ other metadata │
└────────┬────────┘
│
▼
┌─────────┐
│ Object │
└─────────┘
Multiple shared_ptrs share the same control block.
The exact implementation details are implementation-dependent, but this is the
correct interview-level model.
What is use_count()?
use_count() returns the number of shared_ptr instances that currently
share ownership of the same object.
auto p1 = std::make_shared<int>(10);
std::cout << p1.use_count(); // 1
auto p2 = p1;
std::cout << p1.use_count(); // 2
use_count() counts strong owners, not weak_ptrs.
Also, in multithreaded code, don't use use_count() as a synchronization
mechanism.
What happens when the last shared_ptr is destroyed?
When the strong reference count reaches zero, the managed object is destroyed. The control block
may remain until all associated weak_ptrs are gone.
Conceptually:
shared_ptr
↓
strong count = 0
↓
object destroyed
But:
weak_ptr still exists
↓
control block may remain
Once there are no strong or weak references requiring the control block, the control block is released.
Why does weak_ptr not increase reference count?
Because weak_ptr is non-owning. If it increased the strong reference count, it could
prevent the object from being destroyed.
Example:
auto p = std::make_shared<int>(10);
std::weak_ptr<int> w = p;
The weak pointer doesn't keep 10 alive.
When:
p.reset();
the object can be destroyed even though w still exists.
What is a circular reference?
A circular reference occurs when objects owned by shared_ptr indirectly or directly
own each other, keeping their reference counts above zero forever.
Example:
struct B;
struct A {
std::shared_ptr<B> b;
};
struct B {
std::shared_ptr<A> a;
};
Now:
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a;
Conceptually:
a ──shared──► b ▲ │ └────shared───┘
Even after external pointers disappear:
strong count of A > 0
strong count of B > 0
So neither object is destroyed.
This causes a memory leak.
How does weak_ptr solve circular references?
Replace one of the owning shared_ptr relationships with a weak_ptr, so
the cycle does not keep the objects alive.
Instead of:
struct B {
std::shared_ptr<A> a;
};
use:
struct B {
std::weak_ptr<A> a;
};
Now:
A ──shared──► B ▲ │ └────weak─────┘
The weak edge doesn't increase the strong count.
Therefore, when external owners disappear, both objects can be destroyed.
Typical examples
- Parent-child relationships
- Graph structures
- Observer patterns
- Caches
- Bidirectional relationships
What is weak_ptr::lock()?
lock() attempts to obtain a shared_ptr from a weak_ptr. If
the object is still alive, it returns a valid shared_ptr; otherwise it returns an
empty shared_ptr.
Code
auto shared = std::make_shared<int>(10);
std::weak_ptr<int> weak = shared;
if (auto p = weak.lock()) {
std::cout << *p;
}
If the object is already destroyed:
if (auto p = weak.lock()) {
// object exists
} else {
// object already destroyed
}
Why use lock()?
You should not simply assume that an observed object is still alive.
lock() safely attempts to create a temporary owning reference.
make_unique vs new
make_unique is generally preferred because it creates the object and
unique_ptr together, making ownership clearer and safer.
Prefer:
auto p = std::make_unique<int>(10);
Instead of:
std::unique_ptr<int> p(new int(10));
Advantages
- Cleaner syntax
- Clearly expresses ownership
- Safer construction pattern
- Avoids directly handling raw owning pointer
For normal ownership, I prefer make_unique over explicitly using new.
make_shared vs shared_ptr(new T)
make_shared is generally preferred because it is cleaner and can often allocate
the object and control block together, reducing allocation overhead.
Prefer:
auto p = std::make_shared<int>(10);
Instead of:
std::shared_ptr<int> p(new int(10));
Conceptually:
make_shared
one allocation ┌─────────────────────────┐ │ control block + object │ └─────────────────────────┘
shared_ptr(new T)
Often involves separate allocations:
control block ───► object
make_shared is not universally best for every unusual ownership/deleter
requirement, but it is the normal default.
Can shared_ptr cause memory leaks?
Yes. The most common case is a circular reference where shared_ptrs keep each other
alive.
Example:
struct A {
std::shared_ptr<B> b;
};
struct B {
std::shared_ptr<A> a;
};
This can create:
A ──► B ▲ │ └─────┘
Use:
std::weak_ptr<A>
for the non-owning direction.
Other issue
Using shared_ptr everywhere can also create unnecessarily complex ownership graphs
and overhead.
When should you use raw pointers?
Raw pointers are appropriate when they are non-owning observers or when interfacing with APIs that require raw pointers.
Example:
void print(const int* p) {
if (p) {
std::cout << *p;
}
}
The function doesn't own the object.
Another example
auto p = std::make_unique<int>(10);
int* observer = p.get();
Here:
unique_ptr → owns raw pointer → observes
The raw pointer must not call:
delete observer;
Think:
Who owns it?
↓ smart pointer
Who only observes it?
↓ reference / raw pointer
Ownership vs observation
This is one of the most important smart-pointer concepts.
Ownership
The pointer is responsible for keeping the resource alive.
Examples:
std::unique_ptr<T>
std::shared_ptr<T>
Observation
The pointer only accesses an object owned somewhere else.
Examples:
T*
T&
std::weak_ptr<T>
Example
class Engine {};
class Car {
std::unique_ptr<Engine> engine;
public:
Engine* getEngine() {
return engine.get();
}
};
Here:
Car ↓ unique_ptr ↓ Engine
Car owns Engine.
But the returned Engine* is only an observer.
The most important question when choosing a pointer type is whether the pointer owns the object or only observes it.
Smart pointer as a function parameter
The correct parameter type depends on the ownership semantics.
Case 1 — Function doesn't own object
Prefer reference:
void process(const Resource& resource);
or:
void process(Resource* resource);
if null is meaningful.
Case 2 — Function takes ownership
Use unique_ptr by value:
void process(std::unique_ptr<Resource> resource) {
// owns resource
}
Call:
auto p = std::make_unique<Resource>();
process(std::move(p));
Case 3 — Function shares ownership
Use shared_ptr by value:
void process(std::shared_ptr<Resource> resource) {
}
Passing by value means the function participates in shared ownership.
Case 4 — Function only temporarily observes shared-owned object
Usually use:
void process(const Resource& resource);
or:
void process(const Resource* resource);
rather than unnecessarily passing shared_ptr.
Don't pass a smart pointer merely because the caller happens to have one. Choose the parameter type based on the function's ownership requirement.
Smart pointer as a return value
Returning a smart pointer is useful when the returned object has dynamic lifetime and ownership needs to be transferred or shared.
Returning unique_ptr
std::unique_ptr<Resource> createResource() {
return std::make_unique<Resource>();
}
This is a very common pattern.
Ownership is transferred to the caller.
Returning shared_ptr
std::shared_ptr<Resource> createResource() {
return std::make_shared<Resource>();
}
Use this when shared ownership is actually required.
Don't return a raw pointer to a local object:
Resource* create() {
Resource r;
return &r; // WRONG
}
r is destroyed when the function returns.
Design a class that owns a resource
Suppose we have a File resource.
Good design
#include <memory>
#include <string>
class File {
public:
File(const std::string& name) {
// open file
}
~File() {
// close file
}
};
class Logger {
private:
std::unique_ptr<File> file;
public:
Logger(const std::string& name)
: file(std::make_unique<File>(name)) {}
};
Why unique_ptr?
Because:
Logger owns File
There is one clear owner.
I would model exclusive ownership using unique_ptr and rely on RAII so the
resource is automatically released when the owning object is destroyed.
Implement a smart pointer
This is a common interview coding question.
A simplified unique_ptr:
template <typename T>
class MyUniquePtr {
private:
T* ptr;
public:
explicit MyUniquePtr(T* p = nullptr)
: ptr(p) {}
~MyUniquePtr() {
delete ptr;
}
MyUniquePtr(const MyUniquePtr&) = delete;
MyUniquePtr& operator=(const MyUniquePtr&) = delete;
MyUniquePtr(MyUniquePtr&& other) noexcept
: ptr(other.ptr) {
other.ptr = nullptr;
}
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
T& operator*() const {
return *ptr;
}
T* operator->() const {
return ptr;
}
T* get() const {
return ptr;
}
};
Usage:
MyUniquePtr<int> p(new int(10));
std::cout << *p;
What concepts are being tested?
- RAII
- Destructor
- Ownership
- Copy prevention
- Move constructor
- Move assignment
std::move- Exception safety
Why delete copy operations?
Because the class represents exclusive ownership.
Why set other.ptr = nullptr?
To prevent the moved-from object from deleting the resource.
Implement reference counting
A simplified shared_ptr implementation:
template <typename T>
class MySharedPtr {
private:
T* ptr;
size_t* count;
void release() {
if (count && --(*count) == 0) {
delete ptr;
delete count;
}
}
public:
explicit MySharedPtr(T* p = nullptr)
: ptr(p),
count(p ? new size_t(1) : nullptr) {}
MySharedPtr(const MySharedPtr& other)
: ptr(other.ptr),
count(other.count) {
if (count) {
++(*count);
}
}
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
release();
ptr = other.ptr;
count = other.count;
if (count) {
++(*count);
}
}
return *this;
}
~MySharedPtr() {
release();
}
size_t use_count() const {
return count ? *count : 0;
}
T* get() const {
return ptr;
}
};
Usage:
MySharedPtr<int> p1(new int(10));
MySharedPtr<int> p2 = p1;
std::cout << p1.use_count(); // 2
This is a simplified educational implementation, not a production-quality
shared_ptr.
A real implementation needs to handle things such as:
- Thread-safe reference counting
- Weak references
- Control block
- Custom deleters
- Aliasing constructors
- Exception safety
- More subtle ownership cases
Implement reference counting
A simplified shared_ptr implementation:
template <typename T>
class MySharedPtr {
private:
T* ptr;
size_t* count;
void release() {
if (count && --(*count) == 0) {
delete ptr;
delete count;
}
}
public:
explicit MySharedPtr(T* p = nullptr)
: ptr(p),
count(p ? new size_t(1) : nullptr) {}
MySharedPtr(const MySharedPtr& other)
: ptr(other.ptr),
count(other.count) {
if (count) {
++(*count);
}
}
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
release();
ptr = other.ptr;
count = other.count;
if (count) {
++(*count);
}
}
return *this;
}
~MySharedPtr() {
release();
}
size_t use_count() const {
return count ? *count : 0;
}
T* get() const {
return ptr;
}
};
Usage:
MySharedPtr<int> p1(new int(10));
MySharedPtr<int> p2 = p1;
std::cout << p1.use_count(); // 2
This is a simplified educational implementation, not a production-quality
shared_ptr.
A real implementation needs to handle things such as:
- Thread-safe reference counting
- Weak references
- Control block
- Custom deleters
- Aliasing constructors
- Exception safety
- More subtle ownership cases
Convert raw-pointer code to RAII
Suppose we have:
void process() {
Resource* resource = new Resource();
doSomething(resource);
delete resource;
}
Problem
If:
doSomething(resource);
throws an exception:
new ↓ doSomething() ↓ exception ↓ delete never executes ↓ memory leak
Convert to RAII
void process() {
auto resource = std::make_unique<Resource>();
doSomething(resource.get());
}
Now:
function starts
↓
unique_ptr owns Resource
↓
doSomething()
↓
exception or normal return
↓
unique_ptr destructor
↓
Resource destroyed
Better design
If possible, make the resource itself RAII-based instead of manually managing its lifetime.
STL Containers & Iterators
What is STL?
STL, or Standard Template Library, is a collection of generic C++ components such as containers, algorithms, iterators, and function objects.
Simple explanation
Instead of implementing everything yourself:
vector
map
set
sort
find
queue
stack
you can use the standard library.
Example:
#include <vector>
#include <algorithm>
std::vector<int> v = {4, 2, 1, 3};
std::sort(v.begin(), v.end());
What are the major components of STL?
The major components are:
Containers
Algorithms
Iterators
Function objects / functors
Allocators
Example
std::sort(v.begin(), v.end());
Here:
vector → container begin/end → iterators sort → algorithm
What is a container?
A container is an STL class that stores and manages a collection of objects.
Examples:
vector
list
deque
array
set
map
unordered_map
stack
queue
Different containers provide different:
- memory layouts
- access methods
- insertion/deletion characteristics
- ordering
- complexity guarantees
What is vector?
vector is a dynamically sized contiguous array.
std::vector<int> v = {10, 20, 30};
v.push_back(40);
std::cout << v[2];
Conceptually:
10 | 20 | 30 | 40 ↑ ↑ ↑ ↑ contiguous memory
- Dynamic size
- Contiguous storage
- O(1) random access
- O(1) amortized
push_back - Excellent cache locality
Don't say:
vector insertion is always O(1).
push_back() is amortized O(1), but reallocation can make an
individual operation O(n).
What is list?
std::list is a doubly linked list.
Conceptually:
10 ⇄ 20 ⇄ 30 ⇄ 40
Each node typically contains:
previous pointer
data
next pointer
Advantages
Insertion/deletion at a known position:
O(1)
after obtaining the iterator.
Disadvantages
- No random access
- Extra memory for pointers
- Poor cache locality
- Traversal is slower in practice than vector
std::list<int> l = {10, 20, 30};
auto it = std::next(l.begin());
l.insert(it, 15);
What is deque?
deque is a double-ended queue that supports efficient insertion and deletion at both
ends.
std::deque<int> d;
d.push_front(10);
d.push_back(20);
Conceptually:
front back ↓ ↓ 10 | 20 | 30 | 40 | 50
Unlike vector, its elements are not guaranteed to be contiguous.
It supports random access:
d[3];
What is array?
std::array is a fixed-size container that stores elements contiguously and has its
size known at compile time.
std::array<int, 3> a = {10, 20, 30};
Unlike a C-style array, it provides STL functionality:
a.size();
a.begin();
a.end();
Its size cannot change:
std::array<int, 3> a;
// a.push_back(10); // ERROR
What is forward_list?
forward_list is a singly linked list optimized for minimal memory overhead and
forward traversal.
Conceptually:
Compared with list:
forward_list → one link list → two links
It does not support:
l.back();
l.size();
in the same way as list.
Use it when you specifically need a singly linked list and its memory characteristics are beneficial.
What is stack?
stack is a container adaptor that follows LIFO — Last In, First Out.
std::stack<int> s;
s.push(10);
s.push(20);
s.push(30);
s.pop();
Conceptually:
30 ← top
20
10
pop() removes 30.
Common operations
s.push(x);
s.pop();
s.top();
s.empty();
s.size();
What is queue?
queue is a container adaptor that follows FIFO — First In, First Out.
std::queue<int> q;
q.push(10);
q.push(20);
q.push(30);
q.pop();
Conceptually:
front back ↓ ↓ 10 → 20 → 30
pop() removes 10.
What is priority_queue?
priority_queue is a container adaptor where the highest-priority element is
available at the top.
By default it is a max-heap.
std::priority_queue<int> pq;
pq.push(10);
pq.push(50);
pq.push(20);
std::cout << pq.top(); // 50
Min-heap
std::priority_queue<
int,
std::vector<int>,
std::greater<int>
> pq;
Now:
top = smallest element
Common interview use
- Top K problems
- Dijkstra
- Scheduling
- Heap problems
- Median-related problems
What is set?
set stores unique elements in sorted order.
std::set<int> s = {30, 10, 20, 10};
10 20 30
Duplicate 10 is removed.
Typical implementation:
balanced search tree
Usually a red-black tree.
Operations are generally:
O(log n)
What is multiset?
multiset is like set, but it allows duplicate values.
std::multiset<int> ms = {
10, 20, 10, 30
};
10 10 20 30
Search/insertion/deletion are generally O(log n).
What is map?
map stores key-value pairs with unique keys in sorted key order.
std::map<int, std::string> m;
m[2] = "Bob";
m[1] = "Alice";
m[3] = "John";
Iteration:
1 → Alice 2 → Bob 3 → John
Typical implementation is a self-balancing binary search tree (such as a Red-Black Tree).
Operations:
search O(log n)
insert O(log n)
erase O(log n)
What is multimap?
multimap stores key-value pairs in sorted key order and allows duplicate keys.
std::multimap<int, std::string> m;
m.insert({1, "Alice"});
m.insert({1, "Bob"});
Both entries exist.
1 → Alice 1 → Bob
Unlike map, this doesn't work the same way:
m[1] = "Alice"; // multimap doesn't provide operator[]
What is unordered_set?
unordered_set stores unique elements using hashing and does not maintain sorted
order.
std::unordered_set<int> s;
s.insert(10);
s.insert(20);
s.insert(10);
Contains:
10
20
Average operations:
O(1)
Worst case:
O(n)
What is unordered_map?
unordered_map stores key-value pairs using hashing, with unique keys and no
sorted-order guarantee.
std::unordered_map<std::string, int> freq;
freq["apple"]++;
freq["banana"]++;
freq["apple"]++;
apple → 2
banana → 1
Average:
O(1)
Worst case:
O(n)
vector vs list
| Feature | vector | list |
|---|---|---|
| Storage | Contiguous | Linked nodes |
| Random access | O(1) | O(n) |
push_back |
Amortized O(1) | O(1) |
| Insert known position | O(n) | O(1) |
| Cache locality | Excellent | Poor |
| Memory overhead | Low | Higher |
vector vs deque
| Feature | vector | deque |
|---|---|---|
| Contiguous | Yes | No |
| Random access | O(1) | O(1) |
push_back |
Amortized O(1) | O(1) |
push_front |
O(n) | O(1) |
| Memory layout | One contiguous block | Multiple blocks |
Use vector by default. Use deque when efficient insertion/removal at both front and back is required.
array vs vector
| Feature | array | vector |
|---|---|---|
| Size | Fixed | Dynamic |
| Resize | No | Yes |
| Contiguous | Yes | Yes |
size() |
Yes | Yes |
| Heap allocation | Not inherently | Usually dynamically allocated storage |
Example:
std::array<int, 5> a;
std::vector<int> v;
Use array when the size is fixed and vector when the size needs to change.
stack vs queue
stack → LIFO queue → FIFO
map vs unordered_map
| Feature | map | unordered_map |
|---|---|---|
| Structure | Tree | Hash table |
| Ordering | Sorted | No sorted order |
| Average lookup | O(log n) | O(1) |
| Worst lookup | O(log n) | O(n) |
| Range queries | Excellent | Not ordered |
| Hash required | No | Yes |
When to use map
Use when you need:
sorted keys
lower_bound
upper_bound
range queries
predictable O(log n)
When to use unordered_map
Use when you mainly need:
fast average lookup
no ordering requirement
set vs unordered_set
Same fundamental difference:
set → ordered tree unordered_set → hash table
set
sorted
O(log n)
unordered_set
not sorted
average O(1)
map vs multimap
map
Unique keys:
std::map<int, std::string> m;
m[1] = "Alice";
m[1] = "Bob";
The second assignment changes the value.
multimap
Duplicate keys:
std::multimap<int, std::string> m;
m.insert({1, "Alice"});
m.insert({1, "Bob"});
Both exist.
set vs multiset
set → unique values multiset → duplicates allowed
Example:
std::set<int> s = {1, 1, 2};
// {1, 2}
std::multiset<int> ms = {1, 1, 2};
// {1, 1, 2}
Complexity of vector operations
| Operation | Complexity |
|---|---|
| Access v[i] | O(1) |
push_back |
Amortized O(1) |
pop_back |
O(1) |
| Insert middle | O(n) |
| Erase middle | O(n) |
| Search unsorted | O(n) |
size() |
O(1) |
Complexity of list operations
| Operation | Complexity |
|---|---|
| Insert with iterator | O(1) |
| Erase with iterator | O(1) |
push_front |
O(1) |
push_back |
O(1) |
| Random access | O(n) |
| Search | O(n) |
Important:
Finding the position can still take O(n); the insertion itself is O(1) once you already have the iterator.
Complexity of deque
| Operation | Complexity |
|---|---|
| Random access | O(1) |
push_front |
O(1) |
push_back |
O(1) |
pop_front |
O(1) |
pop_back |
O(1) |
| Middle insertion | O(n) |
Complexity of map
Typically:
search → O(log n) insert → O(log n) erase → O(log n)
because it is generally implemented using a balanced tree.
Complexity of unordered_map
Average:
search → O(1) insert → O(1) erase → O(1)
Worst case:
O(n)
Average vs worst-case complexity of hash tables
Hash tables provide average O(1) lookup when the hash function distributes keys well, but worst-case lookup can be O(n) if many keys collide into the same bucket.
Conceptually:
good hashing:
key → hash → bucket
↓
one/few items
Bad collisions:
key1 ─┐ key2 ─┤ key3 ─┼──► same bucket key4 ─┘
Then searching can approach O(n).
What is vector capacity?
Capacity is the amount of storage currently allocated by a vector, while size is the number of elements currently stored.
Example:
std::vector<int> v;
v.reserve(100);
Then:
size = 0
capacity = 100
After:
v.push_back(10);
size = 1
capacity = 100
size() vs capacity()
std::vector<int> v;
v.reserve(10);
std::cout << v.size(); // 0
std::cout << v.capacity(); // at least 10
size = how many elements exist
capacity = how many elements can fit without reallocation
reserve() vs resize()
This is a very common interview question.
reserve()
Changes capacity:
std::vector<int> v;
v.reserve(100);
size = 0
capacity ≥ 100
It does not create 100 elements.
resize()
Changes size:
v.resize(100);
Now:
size = 100
The elements are value-initialized as appropriate.
reserve() allocates storage without changing the number of elements, while
resize() changes the number of elements.
How does vector grow?
When capacity is insufficient:
v.push_back(x);
the vector generally:
1. Allocates larger storage
2. Moves/copies existing elements
3. Constructs the new element
4. Releases old storage
Conceptually:
Old:
[10][20][30]
↑
capacity full
↓ reallocation
New:
[10][20][30][40][ ][ ]
The exact growth factor is implementation-dependent.
Don't claim:
vector always doubles.
That is not guaranteed by the C++ standard.
What happens during vector reallocation?
Suppose:
std::vector<int> v;
v.reserve(2);
v.push_back(10);
v.push_back(20);
int* p = &v[0];
v.push_back(30);
Capacity may no longer be enough.
The vector allocates new storage:
Old storage [10][20] ↓ move/copy ↓ New storage [10][20][30]
Then old storage is released.
Pointers, references, and iterators to elements of the old storage are invalidated.
This is extremely important.
Why is push_back() amortized O(1)?
Although an individual push_back() can take O(n) during reallocation, capacity
grows geometrically, so the total cost over many insertions is O(n), giving O(1) amortized
cost per insertion.
Example:
capacity:
Most insertions don't require reallocation.
Only occasional insertions do expensive work.
Therefore:
n push_back operations
≈ O(n) total
per operation
≈ O(1) amortized
Amortized O(1) does not mean every operation is O(1).
push_back() vs emplace_back()
push_back
Adds an existing object/value:
std::vector<std::pair<int, std::string>> v;
v.push_back({1, "Alice"});
emplace_back
Constructs the element directly in the vector:
v.emplace_back(1, "Alice");
emplace_back constructs the element directly in the container using forwarded
arguments, while push_back inserts an existing object or temporary.
Don't say:
emplace_back is always faster.
Not necessarily. Modern C++ move/optimization can make the difference negligible in many cases.
When does vector invalidate iterators?
Important cases:
Reallocation
If an insertion causes reallocation:
all iterators/references/pointers to elements
→ invalidated
erase
Iterators/references to erased elements are invalidated.
Iterators to elements after the erased position are also affected because elements are shifted.
insert
Without reallocation, iterators/references at or after the insertion position are generally invalidated.
Vector iterator invalidation depends on whether reallocation occurs and where the insertion/erasure happens. Reallocation invalidates all references, pointers, and iterators to its elements.
What happens to references after vector reallocation?
Suppose:
std::vector<int> v;
v.push_back(10);
int& ref = v[0];
v.push_back(20);
If the second push_back causes reallocation:
ref → OLD memory
But the element now lives in:
NEW memory
Therefore:
ref is invalidated and using it afterward is undefined behavior.
Same applies to pointers and iterators.
What is an iterator?
An iterator is an object used to traverse elements of an STL container.
Example:
std::vector<int> v = {10, 20, 30};
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it;
}
Here:
it → 10 it → 20 it → 30
Why are iterators called generalized pointers?
Iterators provide pointer-like operations for traversing different types of containers while hiding the underlying container implementation.
For vector:
*it ++it
For list:
*it ++it
But internally:
vector → pointer-like traversal list → node-to-node traversal
The iterator provides a common interface.
This allows algorithms such as:
std::find(begin, end, value);
to work with many containers.
Input iterator
An input iterator is used to read elements sequentially, generally in a single-pass manner.
Typical capabilities:
read
increment
compare
Example use cases include input streams.
std::istream_iterator<int> it(std::cin);
You generally cannot assume you can move backward or traverse multiple times.
Output iterator
An output iterator is used to write values to a sequence, generally in a single-pass manner.
Example:
std::ostream_iterator<int> out(std::cout, " ");
*out = 10;
++out;
*out = 20;
It is primarily for output rather than reading existing values.
Forward iterator
A forward iterator supports reading/writing as allowed by the iterator and moving forward multiple times through a range.
It supports:
++it
but not:
--it
Example:
forward_list
Bidirectional iterator
A bidirectional iterator can move both forward and backward.
Supports:
++it;
--it;
Example:
list
set
map
Random-access iterator
A random-access iterator supports jumping directly to positions and arithmetic operations.
For example:
it + 5
it - 2
it[3]
it1 - it2
Example:
vector
deque
array
Complexity
Jumping:
it + 1000
is O(1).
It doesn't need to walk through 1000 elements.
Iterator hierarchy
Remember:
Input ↑ Forward ↑ Bidirectional ↑ Random Access
Output is a separate write-oriented category.
Practical examples
| Container | Iterator capability |
|---|---|
| vector | Random access + contiguous |
| deque | Random access |
| array | Random access + contiguous |
| list | Bidirectional |
forward_list |
Forward |
| set | Bidirectional |
| map | Bidirectional |
What is const_iterator?
const_iterator allows us to traverse a container without modifying its elements
through the iterator.
std::vector<int> v = {10, 20, 30};
for (auto it = v.cbegin(); it != v.cend(); ++it) {
std::cout << *it;
}
This is invalid:
//*it = 100; // ERROR
const_iterator means:
You cannot modify the element through that iterator.
It does not necessarily mean the entire container can never change through another
non-const access path.
What is reverse_iterator?
A reverse iterator traverses a container from the end toward the beginning.
std::vector<int> v = {10, 20, 30};
for (auto it = v.rbegin(); it != v.rend(); ++it) {
std::cout << *it << " ";
}
30 20 10
With a reverse iterator:
++it
moves toward the beginning of the container.
begin() vs cbegin()
begin()
Returns:
iterator
for a non-const container.
It can modify elements:
auto it = v.begin();
*it = 100;
cbegin()
Returns:
const_iterator
auto it = v.cbegin();
// *it = 100; // ERROR
cbegin() explicitly gives a const iterator, preventing modification
through the iterator.
What is iterator invalidation?
Iterator invalidation occurs when a container operation makes an existing iterator no longer refer to a valid element.
Example:
std::vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4);
If push_back() causes reallocation:
old iterator
↓
old memory ❌
it is invalid.
Using it afterward is undefined behavior.
Range-based for loop
A range-based for loop provides a simpler way to iterate over all elements in a range or container.
std::vector<int> v = {10, 20, 30};
for (int x : v) {
std::cout << x;
}
Conceptually, it uses begin() and end().
Modify elements
for (int& x : v) {
x *= 2;
}
Read-only
for (const int& x : v) {
std::cout << x;
}
Avoid unnecessary copies
Instead of:
for (auto x : v)
use:
for (const auto& x : v)
when the elements are expensive to copy and you only need to read them.
STL Algorithms, Lambdas & Templates
What is std::sort()?
std::sort() rearranges elements in a range into sorted order. By default, it sorts
in ascending order.
std::vector<int> v = {4, 1, 3, 2};
std::sort(v.begin(), v.end());
1 2 3 4
Descending order
std::sort(v.begin(), v.end(), std::greater<int>());
std::sort() requires a random-access iterator, so it works
with:
vector
deque
array
but not directly with:
list
forward_list
For list, use:
list.sort();
How does std::sort() work?
std::sort() is typically implemented using an introspective sorting algorithm,
commonly called introsort, which combines quicksort, heapsort, and insertion
sort.
Conceptually:
introsort │ ├── quicksort-style partitioning │ ├── switch to heapsort if recursion gets too deep │ └── insertion sort for small ranges
Complexity
Average: O(n log n)
Worst: O(n log n)
The standard specifies the complexity requirement, not a particular implementation strategy.
So say:
"Typically implemented using introsort."
rather than:
"C++ always uses quicksort."
sort() vs stable_sort()
sort()
std::sort(v.begin(), v.end());
Equal elements are not guaranteed to preserve their original relative order.
stable_sort()
std::stable_sort(v.begin(), v.end());
Equivalent elements preserve their relative ordering.
Example:
struct Student {
std::string name;
int marks;
};
Suppose:
A → 90 B → 80 C → 90
Stable sorting by marks:
B → 80 A → 90 C → 90
A remains before C.
Use stable_sort when the relative order of equivalent elements matters.
Otherwise, sort is usually sufficient and may have lower overhead.
What is find()?
std::find() searches for a specific value in a range and returns an iterator to the
first matching element.
std::vector<int> v = {10, 20, 30};
auto it = std::find(v.begin(), v.end(), 20);
if (it != v.end()) {
std::cout << "Found";
}
Complexity
O(n)
for a general unsorted range.
What is find_if()?
find_if() finds the first element that satisfies a given condition.
std::vector<int> v = {1, 3, 8, 5};
auto it = std::find_if(
v.begin(),
v.end(),
[](int x) {
return x % 2 == 0;
}
);
It finds:
8
Key difference
find → search for a value find_if → search using a condition
What is count()?
std::count() counts how many times a specific value occurs in a range.
std::vector<int> v = {1, 2, 2, 3, 2};
int c = std::count(v.begin(), v.end(), 2);
std::cout << c; // 3
Complexity:
O(n)
What is count_if()?
count_if() counts elements that satisfy a condition.
int c = std::count_if(
v.begin(),
v.end(),
[](int x) {
return x % 2 == 0;
}
);
This counts even numbers.
Difference
count → exact value count_if → condition
What is lower_bound()?
lower_bound() returns an iterator to the first element that is not less
than the given value.
In other words:
first element >= target
Example:
std::vector<int> v = {1, 3, 3, 5, 7};
auto it = std::lower_bound(v.begin(), v.end(), 3);
Points to:
3
For:
std::lower_bound(v.begin(), v.end(), 4);
it points to:
5
Complexity
For random-access iterators:
O(log n)
The range must be sorted according to the same ordering used for the search.
What is upper_bound()?
upper_bound() returns an iterator to the first element that is greater
than the given value.
In other words:
first element > target
Example:
std::vector<int> v = {1, 3, 3, 5, 7};
auto it = std::upper_bound(v.begin(), v.end(), 3);
Points to:
5
lower_bound → >= x upper_bound → > x
What is binary_search()?
std::binary_search() checks whether a value exists in a sorted range.
std::vector<int> v = {1, 3, 5, 7};
bool found = std::binary_search(
v.begin(),
v.end(),
5
);
Returns:
true
Difference from lower_bound
binary_search():
only tells whether it exists
lower_bound():
gives an iterator to the position
What is min_element()?
min_element() returns an iterator to the smallest element in a range.
auto it = std::min_element(v.begin(), v.end());
std::cout << *it;
Complexity:
O(n)
What is max_element()?
max_element() returns an iterator to the largest element in a range.
auto it = std::max_element(v.begin(), v.end());
std::cout << *it;
Complexity:
O(n)
It returns an iterator, not the value itself.
What is reverse()?
std::reverse() reverses the elements in a range.
std::vector<int> v = {1, 2, 3, 4};
std::reverse(v.begin(), v.end());
4 3 2 1
Complexity:
O(n)
What is accumulate()?
std::accumulate() combines elements of a range into a single result using an initial
value and, optionally, a binary operation.
std::vector<int> v = {1, 2, 3, 4};
int sum = std::accumulate(
v.begin(),
v.end(),
0
);
10
Custom operation
int product = std::accumulate(
v.begin(),
v.end(),
1,
std::multiplies<int>()
);
The type of the initial value matters.
std::accumulate(v.begin(), v.end(), 0);
accumulates as an int.
If you need a long long result:
long long sum =
std::accumulate(v.begin(), v.end(), 0LL);
remove() vs erase()
remove()
std::remove() does not actually reduce the container's
size.
It rearranges the elements and returns a new logical end.
std::vector<int> v = {1, 2, 3, 2, 4};
auto new_end =
std::remove(v.begin(), v.end(), 2);
Conceptually:
before:
1 2 3 2 4
after remove:
1 3 4 ? ?
↑ new_end
The vector still has its original size.
erase()
Actually removes elements from the container.
v.erase(new_end, v.end());
Now the vector contains:
1 3 4
What is the erase-remove idiom?
The erase-remove idiom uses std::remove to move unwanted elements to the end and
erase to actually remove them from a sequence container such as vector.
Classic form:
v.erase(
std::remove(v.begin(), v.end(), 2),
v.end()
);
This removes all 2s.
Why two operations?
Because:
remove → rearranges range erase → changes container size
Since C++20, you can often write:
std::erase(v, 2);
which is much cleaner for supported containers.
For a predicate:
std::erase_if(v, [](int x) {
return x % 2 == 0;
});
What is for_each()?
for_each() applies a callable operation to every element in a range.
std::for_each(
v.begin(),
v.end(),
[](int x) {
std::cout << x << " ";
}
);
It is useful when you want to perform an action on each element.
Modern C++
A range-based loop is often simpler:
for (int x : v) {
std::cout << x;
}
What is transform()?
transform() applies a function to each element and writes the resulting values to an
output range.
Example:
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> result;
result.resize(v.size());
std::transform(
v.begin(),
v.end(),
result.begin(),
[](int x) {
return x * x;
}
);
1 4 9 16
Common interview use
Convert:
without manually writing the loop.
What is a lambda expression?
A lambda is an anonymous function object that can be defined directly where it is needed.
Example:
auto add = [](int a, int b) {
return a + b;
};
std::cout << add(2, 3);
5
Why useful?
Especially useful with STL algorithms:
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
Lambda syntax
General syntax:
[capture](parameters) -> return_type {
// body
}
Example:
[](int a, int b) -> int {
return a + b;
}
The return type can often be omitted:
[](int a, int b) {
return a + b;
}
The compiler deduces it.
What is lambda capture?
Lambda capture specifies which variables from the surrounding scope the lambda can access.
Example:
int x = 10;
auto f = [x]() {
std::cout << x;
};
x is captured.
Important:
parameters → passed when lambda is called captures → taken from surrounding scope
Capture by value
Syntax:
[x]
Example:
int x = 10;
auto f = [x]() {
std::cout << x;
};
The lambda stores its own copy of x.
Changing the original:
x = 20;
does not change the captured copy.
By default, a value-captured variable cannot be modified inside the lambda:
int x = 10;
auto f = [x]() {
// x++; // ERROR
};
Unless the lambda is mutable.
Capture by reference
Syntax:
[&x]
Example:
int x = 10;
auto f = [&x]() {
x++;
};
f();
std::cout << x; // 11
The lambda accesses the original variable.
A reference capture can become dangling if the lambda outlives the referenced object.
For example, returning a lambda that captures a local variable by reference is dangerous:
auto create() {
int x = 10;
return [&x]() {
return x;
};
}
After create() returns, x no longer exists.
What is [=]?
[=] captures odr-used local variables from the surrounding scope by value.
Example:
int a = 10;
int b = 20;
auto f = [=]() {
return a + b;
};
The lambda captures the needed local variables by value.
It doesn't mean "copy literally every variable in scope."
Only variables actually needed by the lambda are captured.
What is [&]?
[&] captures odr-used local variables from the surrounding scope by reference.
int a = 10;
int b = 20;
auto f = [&]() {
a++;
b++;
};
Both refer to the original variables.
Lifetime.
If the lambda outlives the referenced variables, the references can dangle.
What is [this]?
[this] captures the current object's this pointer, allowing a
lambda inside a member function to access the object's members.
class Counter {
private:
int value = 10;
public:
void print() {
auto f = [this]() {
std::cout << value;
};
f();
}
};
The lambda can access:
value
through the current object.
There is also:
[*this]
which captures a copy of the current object rather than capturing the this
pointer itself.
This can be useful when the lambda needs its own object state and may outlive the original object.
What is mutable in a lambda?
mutable allows a lambda to modify its own copies of variables captured by value.
Without mutable:
int x = 10;
auto f = [x]() {
// x++; // ERROR
};
With:
int x = 10;
auto f = [x]() mutable {
x++;
std::cout << x;
};
The lambda's internal copy changes.
But:
original x
does not change.
What is a generic lambda?
A generic lambda uses auto parameters so the same lambda can work with different
types.
auto add = [](auto a, auto b) {
return a + b;
};
Now:
add(2, 3);
add(2.5, 3.5);
add(std::string("A"), std::string("B"));
can work when the operations are valid.
Internally
Generic lambdas are implemented using templated call operators.
Lambda as comparator
Very common in coding interviews.
Sort descending
std::sort(
v.begin(),
v.end(),
[](int a, int b) {
return a > b;
}
);
Sort pairs by second value
std::sort(
v.begin(),
v.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
}
);
Sort students by marks
std::sort(
students.begin(),
students.end(),
[](const Student& a, const Student& b) {
return a.marks > b.marks;
}
);
The comparator should define a strict weak ordering.
Don't write:
return a <= b;
for a standard sorting comparator.
Use:
return a < b;
Lambda vs function pointer
Lambda
auto f = [](int x) {
return x * 2;
};
A lambda has its own unique closure type.
It can also capture variables:
int k = 10;
auto f = [k](int x) {
return x * k;
};
Function pointer
int (*f)(int);
A normal function pointer points to a function and cannot directly store captured state.
A non-capturing lambda can convert to a compatible function pointer:
auto lambda = [](int x) {
return x * 2;
};
int (*ptr)(int) = lambda;
A capturing lambda cannot implicitly convert to a regular function pointer:
int k = 10;
auto lambda = [k](int x) {
return x * k;
};
// int (*ptr)(int) = lambda; // ERROR
Lambda vs functor
A functor is an object that defines operator().
class Multiply {
public:
int operator()(int a, int b) const {
return a * b;
}
};
Usage:
Multiply multiply;
std::cout << multiply(2, 3);
Lambda:
auto multiply = [](int a, int b) {
return a * b;
};
What are templates?
Templates allow us to write generic code that works with different types while being checked and instantiated at compile time.
Example:
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b;
}
Now:
maximum(10, 20);
maximum(2.5, 1.5);
The compiler generates appropriate versions.
Why do we use templates?
Main reasons
- Code reuse
- Type safety
- Generic programming
- Compile-time polymorphism
- Avoid duplicated implementations
Without templates:
int max(int a, int b);
double max(double a, double b);
With template:
template <typename T>
T max(T a, T b);
One generic definition.
What is a function template?
A function template defines a generic function that can work with different types.
template <typename T>
T add(T a, T b) {
return a + b;
}
Usage:
add(2, 3);
add(2.5, 3.5);
The compiler deduces T.
What is a class template?
A class template allows us to define a generic class that
works with different types.
template <typename T>
class Box {
private:
T value;
public:
Box(T v) : value(v) {}
T get() const {
return value;
}
};
Usage:
Box<int> a(10);
Box<std::string> b("Hello");
What is a class template?
A class template allows us to define a generic class that
works with different types.
template <typename T>
class Box {
private:
T value;
public:
Box(T v) : value(v) {}
T get() const {
return value;
}
};
Usage:
Box<int> a(10);
Box<std::string> b("Hello");
What is template specialization?
Template specialization allows us to provide a different implementation for specific types or type patterns.
Primary template:
template <typename T>
class Printer {
public:
void print() {
std::cout << "Generic";
}
};
Specialization:
template <>
class Printer<bool> {
public:
void print() {
std::cout << "Boolean";
}
};
Now:
Printer<int> a;
a.print(); // Generic
Printer<bool> b;
b.print(); // Boolean
What is full specialization?
Full specialization provides a completely specialized implementation for one exact
template type.
Example:
template <typename T>
class Test {
public:
void print() {
std::cout << "Generic";
}
};
template <>
class Test<int> {
public:
void print() {
std::cout << "Integer";
}
};
This specifically specializes:
Test<int>
What is partial specialization?
Partial specialization provides a specialized implementation for a pattern of
template arguments rather than one exact type.
For example:
template <typename T>
class Test {
};
Partial specialization for pointers:
template <typename T>
class Test<T*> {
public:
void print() {
std::cout << "Pointer";
}
};
Now:
Test<int> a; // primary template
Test<int*> b; // partial specialization
Partial specialization is supported for class templates, not
function templates.
For function templates, use overloading or other mechanisms such as constraints.
What are variadic templates?
Variadic templates allow a template to accept a variable number of
template arguments.
Example:
template <typename... Args>
void print(Args... args) {
(std::cout << ... << args);
}
Usage:
print(10, 20, 30);
print("Hello", 10, 2.5);
The ... represents a parameter pack.
Common use cases
std::tuplestd::make_sharedstd::make_unique- Generic logging
- Generic wrappers
- Perfect forwarding
What are Concepts in C++?
Concepts are a feature introduced in C++20 that allows us to
specify requirements that a type must satisfy before it can be used with a
template.
The easiest way to think about concepts is:
A concept is a compile-time rule that says what a template type must be capable of doing.
For example, suppose we write:
template <typename T>
void print(T x) {
cout << x;
}
Here, we're saying:
"Give me any type T."
But that's not really true.
T must support:
cout << x;
Otherwise the code won't compile.
A concept lets us explicitly express that requirement.
Why Were Concepts Introduced?
Before C++20, templates were often written like this:
template <typename T>
void print(T x) {
cout << x;
}
It looks like any type can be passed.
For example:
print(10);
print("Hello");
print(3.14);
These work because those types can be printed.
But suppose:
class Student {
};
and:
Student s;
print(s);
There is no:
cout << s;
So the compiler generates a complicated template-related error.
Older C++ template errors could become extremely long and difficult to understand.
Concepts allow us to say:
"This template only accepts types satisfying this
requirement."
Templates and compile-time polymorphism
Templates provide compile-time polymorphism by selecting or generating type-specific code during
compilation rather than using runtime virtual dispatch.
Example:
template <typename T>
void process(T obj) {
obj.run();
}
If:
class A {
public:
void run() {}
};
class B {
public:
void run() {}
};
Then:
process(A{});
process(B{});
can generate different code at compile time.
Compare
Runtime polymorphism:
class Base {
public:
virtual void run() = 0;
};
runtime dispatch
Templates:
compile-time selection/generation
Templates vs inheritance
This is a very important design question.
Templates
template <typename T>
void process(T& obj) {
obj.run();
}
Use when:
- Types are known at compile time
- Generic behavior is needed
- Static polymorphism is appropriate
- You don't need a common runtime base
class
Inheritance
class Base {
public:
virtual void run() = 0;
};
Use when:
- Objects need a common runtime interface
- Derived types may be selected dynamically
- Runtime polymorphism is required
Comparison
| Templates | Inheritance |
|---|---|
| Compile-time polymorphism | Runtime polymorphism |
Usually no virtual dispatch |
Virtual dispatch possible |
| Often highly optimized | Some runtime overhead |
| Can generate more code | Usually less template instantiation |
| Static type relationships | Runtime substitutability |
| Generic programming | Object-oriented polymorphism |
Type Casting, Virtual Table & Advanced OOP
What are C++ casts?
C++ provides four named casts:
static_castdynamic_castconst_castreinterpret_cast
They are safer and more explicit than C-style casts.
Simple explanation
Each cast has a specific purpose:
| Cast | Main purpose |
|---|---|
static_cast |
Compile-time conversions |
dynamic_cast |
runtime-checked polymorphic casting |
const_cast |
Add/remove const or volatile |
reinterpret_cast |
Low-level reinterpretation of bits/types |
Example:
double d = 10.5;
int x = static_cast<int>(d);
Which cast should you prefer?
Use the most specific C++ cast that expresses your intent. Avoid C-style casts when possible.
Don't say that all four casts perform the same type of conversion.
What is static_cast?
static_cast performs compile-time conversions when the conversion is known to be
valid.
Simple explanation
Common uses:
double d = 10.5;
int x = static_cast<int>(d);
It can also be used for certain inheritance conversions:
class Base {};
class Derived : public Base {};
Derived d;
Base* b = static_cast<Base*>(&d);
It can also convert numeric types, enums, and other explicitly allowed conversions.
Code example
int x = 10;
double y = static_cast<double>(x);
std::cout << y;
static_cast does not perform runtime checking for a downcast.
Base* b = ...;
Derived* d = static_cast<Derived*>(b);
This is only safe if b actually points to a suitable Derived object.
What is dynamic_cast?
dynamic_cast performs runtime-checked casting, mainly within polymorphic inheritance
hierarchies.
Simple explanation
Consider:
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show() {
std::cout << "Derived\n";
}
};
Now:
Base* base = new Derived;
Derived* derived = dynamic_cast<Derived*>(base);
if (derived) {
derived->show();
}
The runtime checks whether the object is actually a Derived.
For downcasting through a base pointer/reference, the base class generally needs
to be polymorphic, meaning it has at least one virtual
function.
Don't say:
"dynamic_cast is always better than static_cast."
It performs runtime checking and has overhead. Use it when runtime type checking is actually needed.
What is const_cast?
const_cast is used to add or remove const or volatile
qualification.
Example
const int x = 10;
int* p = const_cast<int*>(&x);
But this does not make it safe to modify x.
*p = 20; // Undefined behavior
because x was originally declared const.
Safe example
int x = 10;
const int* p = &x;
int* q = const_cast<int*>(p);
*q = 20; // OK
The original object was not actually const.
const_cast does not magically make a genuinely const object
mutable.
What is const_cast?
const_cast is used to add or remove const or volatile
qualification.
Example
const int x = 10;
int* p = const_cast<int*>(&x);
But this does not make it safe to modify x.
*p = 20; // Undefined behavior
because x was originally declared const.
Safe example
int x = 10;
const int* p = &x;
int* q = const_cast<int*>(p);
*q = 20; // OK
The original object was not actually const.
const_cast does not magically make a genuinely const object
mutable.
What is reinterpret_cast?
reinterpret_cast performs low-level reinterpretation of one type as another type. It
should be used very carefully because incorrect use can lead to undefined behavior.
Example
int x = 10;
int* p = &x;
std::uintptr_t address =
reinterpret_cast<std::uintptr_t>(p);
It is commonly associated with low-level systems programming, memory addresses, hardware interfaces, and interoperability.
Why dangerous?
It does not make the underlying object magically become another valid object type.
For example:
double d = 10.5;
int* p = reinterpret_cast<int*>(&d);
Dereferencing p as an int can violate object/lifetime/alignment/aliasing rules.
Don't describe it as simply:
"It converts any type safely."
It is exactly the opposite: it is a low-level, potentially unsafe operation.
C-style cast vs C++ casts
C-style casts are less explicit because they can attempt several kinds of
conversions. C++ casts clearly express the programmer's intent and are easier to review.
C-style:
int x = (int)3.14;
C++:
int x = static_cast<int>(3.14);
Why prefer C++ casts?
They:
- clearly communicate intent
- are easier to search for
- provide more restricted conversions
- make dangerous conversions more obvious
Don't say C-style casts are always equivalent to static_cast.
A C-style cast can attempt combinations involving const_cast,
static_cast, and reinterpret_cast depending on the types.
When should dynamic_cast be used?
Use dynamic_cast when you genuinely need to determine or safely convert the runtime
type within a polymorphic hierarchy.
Example
class Base {
public:
virtual ~Base() = default;
};
class Dog : public Base {
public:
void bark() {
std::cout << "Bark\n";
}
};
class Cat : public Base {
public:
void meow() {
std::cout << "Meow\n";
}
};
void process(Base* obj) {
if (Dog* dog = dynamic_cast<Dog*>(obj)) {
dog->bark();
}
}
If the design constantly needs dynamic_cast, the interviewer may ask whether
polymorphism or virtual functions could provide a cleaner design.
What happens when dynamic_cast fails?
For a pointer cast, it returns nullptr.
For a reference cast, it throws std::bad_cast.
Pointer
Base* base = new Base;
Derived* derived = dynamic_cast<Derived*>(base);
if (derived == nullptr) {
std::cout << "Cast failed\n";
}
Reference
try {
Derived& derived =
dynamic_cast<Derived&>(*base);
}
catch (const std::bad_cast&) {
std::cout << "Cast failed\n";
}
dynamic_cast<T*>(ptr)
↓
nullptr on failure
dynamic_cast<T&>(ref)
↓
std::bad_cast on failure
Can constructors be virtual?
No.
Constructors cannot be virtual because virtual dispatch depends on an
object whose construction is already underway, while the constructor is responsible for creating
that object.
Invalid:
class Base {
public:
virtual Base(); // Error
};
Can destructors be virtual?
Yes.
In a polymorphic base class, the destructor should usually be
virtual if objects may be deleted through a base pointer.
Example
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destroyed\n";
}
};
Base* ptr = new Derived;
delete ptr;
With a virtual destructor:
Derived destructor
↓
Base destructor
A destructor can be virtual, unlike a constructor.
Can a pure virtual function have an implementation?
Yes.
A pure virtual function can have a definition.
class Base {
public:
virtual void show() = 0;
};
void Base::show() {
std::cout << "Base implementation\n";
}
Derived classes can still override it.
Why useful?
A pure virtual function can define an interface while also providing common base
behavior.
What is a pure virtual destructor?
A destructor can be declared pure virtual to make the class abstract,
but it still needs a definition.
class Base {
public:
virtual ~Base() = 0;
};
Base::~Base() {
}
Why definition required?
When a derived object is destroyed, the base destructor is eventually called.
So the base destructor must have a definition.
virtual ~Base() = 0;
makes the class abstract, but:
Base::~Base() {}
is still required.
What does override do?
override tells the compiler that a derived function is intended to
override a virtual function from the base class.
class Base {
public:
virtual void show();
};
class Derived : public Base {
public:
void show() override;
};
If the function doesn't actually override a base virtual function,
the compiler reports an error.
Why useful?
It catches mistakes such as:
void show(int x) override;
when the base has:
virtual void show();
What does final do?
final prevents further overriding of a virtual function or further inheritance from
a class.
Function
class Base {
public:
virtual void show() final {
}
};
Now:
class Derived : public Base {
void show() override; // Error
};
Class
class Base final {
};
Now:
class Derived : public Base { }; // Error
override protects against accidental incorrect overriding.
final prevents further overriding/inheritance.
What is object slicing?
Object slicing occurs when a derived object is copied into a base object by value, causing the derived-specific part to be sliced off.
Example
class Base {
public:
int x = 10;
};
class Derived : public Base {
public:
int y = 20;
};
Derived d;
Base b = d;
b contains only the Base part.
Derived object
+-----------+
| Base part |
+-----------+
| Derived y |
+-----------+
↓ copy by value
Base object
+-----------+
| Base part |
+-----------+
Avoid slicing
Use a base reference or pointer:
Base& b = d;
or:
Base* b = &d;
Base pointer vs derived pointer
Suppose:
class Base {};
class Derived : public Base {};
Base pointer
Base* b = new Derived;
This is an upcast.
The pointer can access only the interface available through Base*.
Derived pointer
Derived* d = new Derived;
It can access both:
Base members + Derived members
The pointer type controls what members are accessible at compile time, while
virtual functions can use the object's dynamic type at runtime.
Base reference vs derived reference
Similar to pointers:
Derived d;
Base& b = d;
This is an upcast.
No new object is created.
The reference simply refers to the base subobject of the existing Derived object.
Why useful?
It allows polymorphism without pointer ownership:
void process(const Base& obj);
A Derived object can be passed to it.
What is upcasting?
Upcasting means converting a derived-class pointer/reference to a
base-class pointer/reference.
Example
Derived d;
Base* base = &d;
Base& ref = d;
This is generally safe because every Derived object contains a Base subobject.
Diagram
Derived ↑ Base view
Actually, conceptually:
Derived object ┌─────────────┐ │ Base part │ ← Base* ├─────────────┤ │ Derived part│ └─────────────┘
Example
class Base {
public:
virtual void show() {
cout << "Base\n";
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived\n";
}
};
int main() {
Derived d;
Base* b = &d; // Upcasting
b->show();
}
Derived
Because show() is virtual, runtime polymorphism calls
Derived::show().
What is downcasting?
Downcasting means converting a base pointer/reference to a derived pointer/reference.
Example
Base* base = new Derived;
Derived* derived =
dynamic_cast<Derived*>(base);
It is potentially unsafe because the base pointer might not actually refer to a Derived object.
Upcasting
Derived → Base
Usually safe.
Downcasting
Base → Derived
Requires validation or a guarantee about the actual object.
What is safe downcasting?
dynamic_cast provides runtime-checked downcasting in a polymorphic hierarchy.
Base* base = new Derived;
Derived* derived =
dynamic_cast<Derived*>(base);
if (derived) {
derived->show();
}
If base does not point to a Derived, the pointer becomes nullptr.
Compared with static_cast
Derived* d = static_cast<Derived*>(base);
static_cast does not perform the same runtime check.
Example: Downcasting fails
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show() {
cout << "Derived object\n";
}
};
int main() {
// This object is actually a Base object
Base* b2 = new Base;
// Try to downcast Base* to Derived*
Derived* d = dynamic_cast<Derived*>(b2);
if (d == nullptr) {
cout << "Downcasting failed\n";
} else {
d->show();
}
delete b2;
}
Downcasting failed
Why does it fail?
Look at this line:
Base* b2 = new Base;
The actual object is:
b2 ↓ ┌──────────────┐ │ Base object │ └──────────────┘
It is not a Derived object.
Then we ask:
Derived* d = dynamic_cast<Derived*>(b2);
dynamic_cast checks the actual runtime type:
Base* b2 │ ▼ Actual object │ ▼ Base │ ✗ │ ▼
Derived?
The answer is no, so:
d == nullptr
Compare with successful downcasting
Base* b1 = new Derived;
Derived* d = dynamic_cast<Derived*>(b1);
Here:
b1 ↓ ┌────────────────┐ │ Derived object │ └────────────────┘
So:
Base* b1 │ ▼ Actual object = Derived │ ✓ │ ▼ Derived*
Therefore:
d != nullptr
The key point
The pointer type is Base* in both cases:
Base* b1 = new Derived;
Base* b2 = new Base;
But the actual object type is different:
This is exactly why dynamic_cast is useful: it checks the actual runtime
object type rather than blindly assuming the cast is valid.
Multithreading & Concurrency
For Autodesk SDE interviews, focus on understanding why synchronization is needed, not just memorizing APIs.
A useful mental model:
Thread ↓ Shared state ↓ Race condition / Data race ↓ Synchronization ├── mutex ├── atomic └── condition_variable ↓ Correct + thread-safe program
Threads
1. What is a thread?
A thread is the smallest unit of execution within a process. Threads in the same process share resources such as memory, while each thread has its own execution state and stack.
Simple explanation
A process can contain multiple threads:
Process ├── Thread 1 → stack ├── Thread 2 → stack ├── Thread 3 → stack │ └── Shared heap / global data
Threads share:
- heap
- global/
staticvariables - code
- open resources
Each thread typically has its own:
- stack
- registers
- program counter
Don't say threads have completely separate memory. Threads within the same process share the process's address space.
2. Process vs Thread
A process is an independent execution environment with its own address space, while threads are execution units within a process and share its address space.
| Process | Thread |
|---|---|
| Separate address space | Shares process address space |
| More expensive to create | Usually cheaper |
| Communication needs IPC mechanisms | Can communicate through shared memory |
| Failure usually isolated | One thread can affect the whole process |
| Has one or more threads | Exists inside a process |
Threads make communication easier but introduce synchronization problems.
3. How do you create a thread in C++?
Use std::thread from the <thread> header.
#include <thread>
#include <iostream>
void work() {
std::cout << "Working\n";
}
int main() {
std::thread t(work);
t.join();
}
The main thread and t execute concurrently when the scheduler allows it.
4. What is std::thread?
std::thread is the standard C++ class used to represent and manage
a thread of execution.
It can execute:
- functions
- lambdas
- function objects
- member functions
Example:
std::thread t([] {
std::cout << "Hello\n";
});
t.join();
Creating a std::thread doesn't mean the work is automatically synchronized
with the main thread. You must manage its lifetime.
5. What is join()?
join() makes the calling thread wait until the target thread finishes execution.
std::thread t(work);
t.join();
std::cout << "Thread finished\n";
The output after join() executes is guaranteed to occur after
work() finishes.
A thread can be joined only once.
6. What is detach()?
detach() separates the thread from the std::thread object, allowing
the thread to continue independently.
std::thread t(work);
t.detach();
After detaching, the std::thread object no longer represents the running thread.
You lose direct control over its lifetime.
For example, this is dangerous:
void start() {
int x = 10;
std::thread t([&] {
std::cout << x;
});
t.detach();
}
x may be destroyed while the detached thread still accesses it.
Prefer join() when you need clear lifetime management.
7. What is joinable()?
joinable() tells whether a std::thread object currently represents
a thread of execution.
std::thread t(work);
if (t.joinable()) {
t.join();
}
A default-constructed thread is not joinable:
std::thread t;
std::cout << t.joinable(); // false
After join() or detach(), it is also no longer joinable.
8. What happens if a joinable thread is destroyed?
If a std::thread object is destroyed while it is still joinable, the program
calls std::terminate().
Example:
{
std::thread t(work);
} // std::terminate()
Correct approach
{
std::thread t(work);
t.join();
}
or:
{
std::thread t(work);
t.detach();
}
This behavior exists to prevent silently abandoning a running thread.
9. Passing arguments to a thread
Example
void add(int a, int b) {
std::cout << a + b;
}
std::thread t(add, 10, 20);
t.join();
Arguments are passed to the callable when the thread is created.
Lambda example
int x = 10;
std::thread t([x] {
std::cout << x;
});
t.join();
Arguments are generally stored/decayed inside the thread's internal state. Special handling is needed when you want to pass references.
10. Passing by reference using std::ref
Suppose:
void increment(int& x) {
++x; }
This won't work as a normal copied argument when you intend reference semantics.
Use:
int x = 10;
std::thread t(increment, std::ref(x));
t.join();
std::cout << x; // 11
Why std::ref?
It creates a reference wrapper that tells the thread machinery to pass the original object by reference.
The referenced object must remain alive while the thread accesses it.
Race Conditions
11. What is a race condition?
A race condition occurs when the program's result depends on the timing or ordering of concurrent operations.
Example:
int counter = 0;
void increment() {
++counter; }
Two threads executing this simultaneously can produce unexpected results.
Why?
Conceptually:
Thread 1: read counter
Thread 2: read counter
Thread 1: increment
Thread 2: increment
Thread 1: write
Thread 2: write
An update can be lost.
12. What is a data race?
A data race occurs when multiple threads access the same memory location concurrently, at least one access is a write, and the accesses are not properly synchronized.
In C++, a data race results in undefined behavior.
Example:
int counter = 0;
void work() {
++counter; }
If two threads execute work() concurrently without synchronization, there is a
data race.
Data race → Undefined behavior
13. Race condition vs data race
This is a very common interview question.
Race condition
A broader concept where correctness depends on execution timing.
Data race
A specific concurrent memory-access problem that violates the language's synchronization rules and causes undefined behavior in C++.
Example
Two threads may race to perform operations where the final result depends on ordering, even if the accesses themselves are synchronized.
So:
Data race ⊂ race-condition-related problems
Not every race condition is necessarily a C++ data race.
14. How do you prevent a data race?
Use proper synchronization or avoid shared mutable state.
Common techniques:
std::mutexstd::atomic- immutable data
- thread-local data
- message passing
Mutex example
std::mutex m;
int counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(m);
++counter; }
Now only one thread at a time modifies counter.
15. What is thread safety?
Thread safety means code behaves correctly when accessed concurrently by multiple threads.
Example:
class Counter {
std::mutex m;
int value = 0;
public:
void increment() {
std::lock_guard<std::mutex> lock(m);
++value;
}
};
Multiple threads can safely call increment().
Thread-safe doesn't necessarily mean lock-free or highly performant.
16. What is shared state?
Shared state is data that can be accessed by multiple threads.
Examples:
int counter;
std::vector<int> data;
std::unordered_map<int, int> cache;
If multiple threads modify shared state, synchronization may be required.
Better design
Reduce shared mutable state whenever possible.
Mutex
17. What is a mutex?
A mutex, or mutual exclusion lock, ensures that only one thread at a time can enter a
protected critical section.
Thread 1 ──┐
↓
Mutex
↓
Critical
Section
↑
Thread 2 ──┘
18. What is std::mutex?
std::mutex is the standard C++ mutual exclusion primitive used to protect shared
data.
std::mutex m;
int counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(m);
++counter; }
19. What does lock() do?
lock() acquires the mutex. If another thread already owns it, the calling thread
waits until it becomes available.
m.lock();
counter++;
m.unlock();
Problem
Manual locking is easy to get wrong.
20. What does unlock() do?
unlock() releases the mutex so another waiting thread can acquire it.
m.lock();
counter++;
m.unlock();
If an exception occurs before unlock():
m.lock();
someFunction(); // throws
m.unlock(); // never reached
The mutex may remain locked.
That's why RAII wrappers are preferred.
21. What is std::lock_guard?
std::lock_guard is a simple RAII wrapper that locks a mutex when constructed and
unlocks it when destroyed.
std::mutex m;
void work() {
std::lock_guard<std::mutex> lock(m);
// critical section
}
When lock goes out of scope:
constructor → lock() destructor → unlock()
Main advantage
It automatically unlocks even when exceptions occur.
22. What is std::unique_lock?
std::unique_lock is a flexible RAII mutex wrapper that supports operations such
as deferred locking, manual unlock/relock, and interaction with condition variables.
Example:
std::unique_lock<std::mutex> lock(m);
counter++;
lock.unlock();
// do other work
lock.lock();
It can also defer locking:
std::unique_lock<std::mutex> lock(m, std::defer_lock);
lock.lock();
More flexible than lock_guard, but has slightly more overhead/complexity.
23. What is std::scoped_lock?
std::scoped_lock is an RAII locking wrapper that can lock one or multiple
mutexes safely.
Example:
std::scoped_lock lock(m1, m2);
It is especially useful when multiple mutexes must be acquired together.
It uses a deadlock-avoidance algorithm for multiple mutexes.
24. lock_guard vs unique_lock
| lock_guard | unique_lock |
|---|---|
| Simple | Flexible |
| Locks immediately | Can defer locking |
| Cannot manually unlock/relock | Can unlock/relock |
| Low overhead | More state/overhead |
| Good for simple critical sections | Required/useful for condition variables |
Use lock_guard when you simply need scoped locking. Use unique_lock
when you need more control over the lock's lifetime or condition variables.
25. unique_lock vs scoped_lock
unique_lock
Best when you need:
- deferred locking
- unlock/relock
- condition variables
std::unique_lock<std::mutex> lock(m);
scoped_lock
Best when you want to acquire multiple mutexes together:
std::scoped_lock lock(m1, m2);
Simple distinction
unique_lock → flexibility scoped_lock → convenient safe locking of multiple mutexes
26. Why is RAII important for mutexes?
RAII ensures the mutex is automatically released when the lock object leaves scope, even if an exception occurs.
{
std::lock_guard<std::mutex> lock(m);
// critical section
}
Conceptually:
enter scope ↓ lock ↓ critical section ↓ leave scope ↓ unlock automatically
This prevents many lock-leak bugs.
27. Why is manual lock/unlock dangerous?
Consider:
m.lock();
doSomething();
m.unlock();
If doSomething() throws:
lock() ↓ exception ↓ unlock() never runs ↓ mutex remains locked
Other threads can block indefinitely.
Better:
std::lock_guard<std::mutex> lock(m);
doSomething();
Deadlocks
28. What is a deadlock?
A deadlock occurs when two or more threads wait indefinitely for resources held by each other.
Example:
Thread 1:
locks A
waits for B
Thread 2:
locks B
waits for A
Neither can continue.
29. What are the four Coffman conditions?
A deadlock requires these four conditions:
1. Mutual exclusion
A resource can be held by only one thread at a time.
2. Hold and wait
A thread holds one resource while waiting for another.
3. No preemption
Resources cannot be forcibly taken away.
4. Circular wait
There is a circular chain of waiting.
T1 → waits for resource held by T2 T2 → waits for resource held by T1
Breaking any one of the four conditions can prevent deadlock.
30. How can deadlocks be prevented?
Common techniques:
- consistent lock ordering
- acquire multiple locks atomically
- use
std::scoped_lock - use
std::lock - minimize lock scope
- avoid unnecessary nested locks
- use
try_lockwith appropriate retry/backoff strategies
Most common answer
Establish a consistent global ordering for acquiring locks.
31. What is lock ordering?
Suppose there are:
std::mutex A;
std::mutex B;
Always acquire:
A → B
Never:
Thread 1: A → B Thread 2: B → A
Because that can create:
T1 holds A → waits for B T2 holds B → waits for A
Instead, all threads follow:
A → B
32. What is std::lock?
std::lock locks multiple mutexes using a deadlock-avoidance algorithm.
Example:
std::lock(m1, m2);
std::lock_guard<std::mutex> l1(m1, std::adopt_lock);
std::lock_guard<std::mutex> l2(m2, std::adopt_lock);
Modern alternative
Usually simpler:
std::scoped_lock lock(m1, m2);
33. What is std::scoped_lock for deadlock prevention?
For multiple mutexes:
std::scoped_lock lock(m1, m2);
It acquires them using a deadlock-avoidance strategy.
This is preferable to manually doing:
m1.lock();
m2.lock();
because different threads could acquire them in opposite orders.
34. What is try_lock?
try_lock() attempts to acquire a mutex without blocking indefinitely.
if (m.try_lock()) {
// acquired
m.unlock();
} else {
// couldn't acquire
}
For multiple locks, std::try_lock can also be used.
Use case
Useful when the program cannot afford to wait indefinitely or wants to perform another action if the lock isn't immediately available.
35. Deadlock vs starvation vs livelock
Deadlock
Threads wait forever for each other.
T1 → waits for T2 T2 → waits for T1
Starvation
A thread keeps getting denied access to a resource while others continue making progress.
T1 → repeatedly gets lock T2 → keeps waiting
Livelock
Threads are active but keep responding to each other without making progress.
T1 → backs off T2 → backs off T1 → retries T2 → retries
...
Deadlock → stuck waiting Starvation → never gets chance Livelock → moving but not progressing
Atomic
36. What is std::atomic?
std::atomic provides atomic operations on shared data, allowing certain
operations to occur without a data race.
Example:
std::atomic<int> counter{0};
void increment() {
++counter; }
Multiple threads can safely increment it.
Atomic does not automatically make a larger multi-step operation atomic.
37. Atomic vs mutex
| Atomic | Mutex |
|---|---|
| Good for simple shared values | Good for complex shared state |
| Supports atomic operations | Protects critical sections |
| Often lower overhead | May involve blocking |
| Can support lock-free implementations | Usually uses locking |
| More difficult for complex invariants | Easier for multi-variable consistency |
Example
Atomic:
std::atomic<int> counter;
counter++;
Mutex:
std::lock_guard<std::mutex> lock(m);
balance -= amount;
log.push_back(amount);
If multiple pieces of state must change together, a mutex is often the better abstraction.
38. When should you use atomic?
Use std::atomic when shared state can be correctly represented through atomic
operations, such as counters, flags, or simple state variables.
Example:
std::atomic<bool> stop{false};
Worker:
while (!stop.load()) {
// work
}
Another thread:
stop.store(true);
Don't replace every mutex with an atomic.
39. What is load()?
load() atomically reads the value of an atomic object.
std::atomic<int> x{10};
int value = x.load();
You can specify memory ordering:
x.load(std::memory_order_relaxed);
40. What is store()?
store() atomically writes a value to an atomic object.
std::atomic<int> x{10};
x.store(20);
Memory ordering can also be specified.
41. What is fetch_add()?
fetch_add() atomically adds a value and returns the previous value.
std::atomic<int> counter{10};
int old = counter.fetch_add(5);
Afterward:
old = 10
counter = 15
It is useful for thread-safe counters.
42. What is compare_exchange?
compare_exchange atomically compares an atomic value with an expected value and
replaces it with a desired value if they match.
Example:
std::atomic<int> x{10};
int expected = 10;
bool success =
x.compare_exchange_strong(expected, 20);
If x == expected:
x becomes 20
success = true
If not:
success = false
expected = actual x
43. What is CAS?
CAS means Compare-And-Swap or Compare-And-Exchange.
Conceptually:
if (current == expected)
current = desired;
else
fail
The operation happens atomically.
Common use
CAS is a fundamental building block for lock-free algorithms.
44. What is lock-free programming?
Lock-free programming uses atomic operations to coordinate threads without traditional mutex locks, with the guarantee that system-wide progress is made even if individual threads are delayed.
Example idea
while (true) {
int old = value.load();
if (value.compare_exchange_weak(old, old + 1))
break;
}
Lock-free does not mean wait-free.
Lock-free guarantees some thread makes progress.
Wait-free aims to guarantee that every operation completes within a bounded number of steps.
45. What is memory ordering?
Memory ordering controls how atomic operations synchronize and how memory accesses are ordered between threads.
C++ provides different memory-ordering guarantees.
Common ones:
relaxed
acquire
release
acq_rel
seq_cst
For interviews, understand:
relaxed → atomicity only acquire → prevents later operations from moving before acquire release → prevents earlier operations from moving after release
46. What is memory_order_relaxed?
memory_order_relaxed guarantees atomicity of the atomic operation but provides
no synchronization ordering with other memory accesses.
Example:
counter.fetch_add(
1,
std::memory_order_relaxed
);
Good for independent counters where you don't need to publish other data.
Atomic? Yes
Synchronization? No
47. What is memory_order_acquire?
An acquire operation prevents subsequent memory operations from being reordered before it and can synchronize with a corresponding release operation.
Typical pattern:
ready.load(std::memory_order_acquire);
If it observes a value stored with release, writes before the release become visible after the acquire.
48. What is memory_order_release?
A release operation prevents preceding memory operations from being reordered after it and can publish those operations to a thread performing a corresponding acquire.
Example:
data = 42;
ready.store(
true,
std::memory_order_release
);
Another thread:
if (ready.load(std::memory_order_acquire)) {
std::cout << data;
}
The release/acquire pair establishes the required synchronization when the acquire observes the released value.
49. Acquire vs release
The easiest way to remember:
Thread 1 Thread 2
data = 42
|
↓
release store ───────────────→ acquire load
|
↓
read data
Release
Publishes previous writes.
Acquire
Makes those published writes visible to the acquiring thread.
Release is generally used when publishing data; acquire is used when consuming that published data.
Synchronization
50. What is condition_variable?
std::condition_variable allows a thread to sleep until another thread signals
that some condition or state may have changed.
It is commonly used with:
std::mutexstd::unique_lock
Example:
std::mutex m;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] {
return ready;
});
}
Another thread:
{
std::lock_guard<std::mutex> lock(m);
ready = true;
}
cv.notify_one();
51. What is the producer-consumer problem?
The producer-consumer problem involves one or more threads producing data into a shared buffer and other threads consuming it safely.
Producer ↓ [ Shared Queue ] ↓ Consumer
The queue must be protected against:
- simultaneous modification
- consuming from an empty queue
- overflowing a bounded queue
Typical solution
Use:
mutex + condition_variable + queue
Example:
std::queue<int> q;
std::mutex m;
std::condition_variable cv;
Producer:
{
std::lock_guard<std::mutex> lock(m);
q.push(10);
}
cv.notify_one();
Consumer:
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] {
return !q.empty();
});
int value = q.front();
q.pop();
52. notify_one() vs notify_all()
notify_one()
Wakes one waiting thread.
cv.notify_one();
Useful when one newly available resource can be consumed by one worker.
notify_all()
Wakes all waiting threads.
cv.notify_all();
All awakened threads re-check the condition.
notify_all() can cause many threads to wake even though only one can proceed,
creating unnecessary contention.
53. Why use a predicate with condition_variable?
Instead of:
cv.wait(lock);
prefer:
cv.wait(lock, [] {
return !q.empty();
});
Why?
Because waking up does not necessarily mean the desired condition is true.
The predicate effectively behaves like:
while (!condition) {
wait();
}
This handles spurious wakeups and state changes correctly.
54. What are spurious wakeups?
A spurious wakeup occurs when a thread waiting on a condition variable wakes up even though no notification corresponding to its desired condition occurred.
Therefore:
cv.wait(lock);
does not mean:
"The condition is definitely true."
Instead, always check the condition again.
Preferred:
cv.wait(lock, [] {
return ready;
});
Conceptually:
while (!ready) {
cv.wait(lock);
}
Never write synchronization logic assuming that one notify_one() or
notify_all() guarantees the condition is true.
Advanced C++ & Interview Traps
This is the final advanced set. For an SDE interview, the most important topics here are:
auto/decltypeconstexpr/noexcept- exception safety
- compilation vs linking
- ODR
- perfect forwarding
- forwarding references
- reference collapsing
- SFINAE vs concepts
- CRTP
volatilevs atomic
I'll keep the same interview format: short answer → simple explanation → code → follow-up → trap.
Modern C++
1. What is auto?
auto lets the compiler deduce the type of a variable from its initializer at
compile time.
auto x = 10; // int
auto y = 3.14; // double
auto name = "Bob"; // const char*
The type is still statically known; auto does not make C++
dynamically typed.
auto x;
is invalid because there is no initializer from which to deduce the type.
2. What is decltype?
decltype obtains the type of an expression without evaluating that expression.
int x = 10;
decltype(x) y = 20;
Here:
decltype(x) → int
decltype follows special rules for value categories.
int x = 10;
decltype(x) a = x; // int
decltype((x)) b = x; // int&
Why?
This distinction is frequently tested.
3. auto vs decltype
| auto | decltype |
|---|---|
| Deduces type from initializer | Determines type of expression |
Similar to template type deduction |
Has special value-category rules |
| Usually removes references/top-level cv in deduction | Can preserve references |
| Requires initializer | Can inspect an expression's type |
Example:
int x = 10;
auto a = x;
decltype(x) b = x;
decltype((x)) c = x;
Types:
a → int b → int c → int&
auto deduces a variable's type from its initializer, while
decltype determines the type associated with an expression.
4. What is constexpr?
constexpr indicates that a variable or function can participate in compile-time
evaluation when given constant-expression inputs.
Example:
constexpr int square(int x) {
return x * x;
}
constexpr int x = square(5);
The compiler can evaluate:
25
at compile time.
constexpr does not necessarily mean:
"This always executes at compile time."
A constexpr function can also be called at runtime with non-constant inputs.
int n;
std::cin >> n;
int x = square(n); // runtime
5. const vs constexpr
const
Means the object cannot be modified through that object after initialization.
const int x = 10;
But its value doesn't necessarily have to be known at compile time:
int n;
std::cin >> n;
const int x = n;
constexpr
Requires the initialized value to be usable as a constant expression.
constexpr int x = 10;
const
→ cannot modify
constexpr
→ compile-time evaluable constant
A constexpr variable is also const.
6. What is noexcept?
noexcept specifies that a function is not expected to throw exceptions.
void process() noexcept {
}
You can also query whether an expression is noexcept:
bool b = noexcept(process());
noexcept is part of the function's exception specification.
If a noexcept function allows an exception to escape,
std::terminate() is called.
7. Why should move constructors often be noexcept?
Because standard containers such as std::vector prefer moving elements during
reallocation when the move constructor is known not to throw.
Example:
class A {
public:
A(A&& other) noexcept {
}
};
When a vector grows:
std::vector<A> v;
the implementation may choose move construction instead of copying if the move is
noexcept.
Why?
If moving could throw halfway through reallocation, maintaining the container's exception guarantee becomes more complicated.
Marking a move constructor noexcept allows containers to safely prefer moving
over copying when appropriate.
8. What is nullptr?
nullptr is the C++11 null pointer literal with type std::nullptr_t.
int* p = nullptr;
Why better than NULL?
NULL is traditionally an integer constant such as 0 or 0L.
nullptr is specifically a null pointer value.
Example:
void f(int);
void f(int*);
f(nullptr); // calls f(int*)
nullptr is not an integer.
9. What is std::optional?
std::optional<T> represents a value that may or may not exist.
Example:
std::optional<int> findValue(bool found) {
if (found)
return 42;
return std::nullopt;
}
Usage:
auto result = findValue(true);
if (result.has_value()) {
std::cout << *result;
}
Why useful?
Instead of using:
-1
nullptr
special value
to indicate "not found", we can explicitly represent absence.
10. What is std::variant?
std::variant is a type-safe union that can hold one value from a
fixed set of types.
std::variant<int, std::string> value;
value = 10;
value = "hello";
Only one alternative is active at a time.
Access
std::get<int>(value);
or:
std::visit([](const auto& x) {
std::cout << x;
}, value);
variant vs union
std::variant provides type safety and manages object lifetimes of the contained
alternatives.
11. What is std::string_view?
std::string_view is a non-owning view of a sequence of characters.
void print(std::string_view s) {
std::cout << s;
}
You can pass:
print("Hello");
or:
std::string str = "Hello";
print(str);
Advantage
It can avoid unnecessary string copies.
It does not own the characters.
std::string_view getName() {
std::string name = "Dilip";
return name; // dangling view
}
The std::string is destroyed when the function returns.
string → owns data string_view → observes data
12. What is std::function?
std::function is a type-erased callable wrapper that can store different
callable objects with a compatible signature.
std::function<int(int, int)> operation;
operation = [](int a, int b) {
return a + b;
};
std::cout << operation(2, 3);
It can hold:
- function pointers
- lambdas
- functors
std::bindresults- other compatible callables
Trade-off
std::function provides flexibility but may have runtime/type-erasure overhead
compared with templates or direct calls.
13. What are functors?
A functor, or function object, is an object that can be called like a function by defining
operator().
class Add {
public:
int operator()(int a, int b) const {
return a + b;
}
};
Usage:
Add add;
std::cout << add(2, 3);
Why useful?
Functors can maintain state.
class Multiplier {
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
14. Function object vs lambda
A lambda is essentially a convenient way to create a function object.
Lambda:
auto add = [](int a, int b) {
return a + b;
};
Functor:
struct Add {
int operator()(int a, int b) const {
return a + b;
}
};
Difference
| Lambda | Functor |
|---|---|
| Concise | More verbose |
| Great for local behavior | Good for reusable behavior |
| Can capture local variables | Can store state as members |
| Compiler creates closure object | Programmer defines class/struct |
Exception Handling
15. What is an exception?
An exception is a mechanism for reporting and handling abnormal conditions separately from normal program flow.
Example:
throw std::runtime_error("File not found");
A caller can handle it:
try {
// code
}
catch (const std::exception& e) {
std::cout << e.what();
}
16. try, catch, throw
try
Contains code that may throw.
try {
process();
}
throw
Signals an exception.
throw std::runtime_error("Error");
catch
Handles the exception.
catch (const std::exception& e) {
std::cout << e.what();
}
Flow
try ↓
exception?
↓ yes throw ↓ matching catch
17. What is exception safety?
Exception safety means ensuring that a program remains correct and resources remain properly managed when exceptions occur.
The main guarantees are:
- basic
- strong
- no-throw
18. What is the basic guarantee?
After an exception, the program remains in a valid state, resources are not leaked, but the object's state may have changed.
Example concept:
operation starts ↓ exception ↓ object remains valid but state may be modified
19. What is the strong guarantee?
If an operation fails, the program state remains unchanged, as if the operation had never happened.
Conceptually:
Before ↓ attempt operation ↓ exception ↓ exactly Before
This is often called the commit-or-rollback guarantee.
20. What is the no-throw guarantee?
The no-throw guarantee means an operation promises not to throw exceptions.
void cleanup() noexcept {
}
The standard library provides many operations with noexcept guarantees.
A noexcept function that allows an exception to escape results in
std::terminate().
21. RAII and exceptions
RAII ensures resources are released automatically when objects leave scope, including when stack unwinding happens due to an exception.
Example:
void process() {
std::lock_guard<std::mutex> lock(m);
doSomething(); // may throw
}
If doSomething() throws, lock is destroyed automatically and releases the mutex.
Same principle
Resource acquisition
↓
Object owns resource
↓
Exception
↓
Destructor
↓
Resource released
This is why RAII is fundamental to exception-safe C++.
22. Why should destructors generally not throw?
Because throwing from a destructor during stack unwinding can cause
std::terminate().
Destructors are generally expected to clean up resources and not fail through exceptions.
Prefer:
~Resource() noexcept {
cleanup();
}
A destructor is implicitly noexcept in many normal cases unless its exception
specification is otherwise affected, and throwing during active exception unwinding is
especially dangerous.
23. What happens if a destructor throws during stack unwinding?
Suppose:
void f() {
Resource r;
throw std::runtime_error("error");
}
During stack unwinding, r's destructor runs.
If the destructor throws another exception:
Original exception
↓
stack unwinding
↓
destructor throws
↓
two active exceptions
↓
std::terminate()
If another exception is thrown while an exception is already being propagated during stack
unwinding, the program calls std::terminate().
Compilation
24. What is the preprocessor?
The preprocessor performs source-level transformations before the actual compilation phase.
Examples:
#include <iostream>
#define MAX 100
#ifdef DEBUG
// ...
#endif
It handles directives such as:
#include#define#if#ifdef
The preprocessor doesn't understand C++ semantics like a compiler does. It primarily performs textual/macro preprocessing.
25. Compilation vs linking
This is a very common interview question.
Compilation
Each source file is compiled into an object file.
main.cpp ↓ compiler ↓ main.o
Linking
The linker combines object files and libraries into the final executable.
main.o foo.o libraries ↓ linker ↓ executable
Example error
If a function is declared but no definition is provided:
void foo();
int main() {
foo();
}
Compilation can succeed, but linking can fail with an undefined reference/unresolved external symbol.
26. What are header files?
Header files typically contain declarations, type definitions, templates, inline
definitions, and other interfaces shared between source files.
Example:
// math.h
int add(int a, int b);
Implementation:
// math.cpp
int add(int a, int b) {
return a + b;
}
Usage:
#include "math.h"
Headers are generally included into translation units by the preprocessor.
27. What are include guards?
Include guards prevent the contents of a header from being processed multiple times within the same translation unit.
#ifndef MY_HEADER_H
#define MY_HEADER_H
class Test {
};
#endif
Without protection, repeated inclusion could cause redefinition errors.
28. What is #pragma once?
#pragma once is a common compiler-supported directive that tells the compiler to include a header only once per translation unit.
#pragma once
class Test {
};
Include guards vs #pragma once
Include guards
→ standard preprocessor technique
#pragma once
→ widely supported but technically non-standard directive
Both prevent repeated header inclusion; include guards are standard C/C++ preprocessing
constructs, while #pragma once is a widely supported compiler extension.
29. Declaration vs definition
Declaration
Tells the compiler that something exists.
int add(int, int);
Definition
Provides the implementation/storage.
int add(int a, int b) {
return a + b;
}
For a variable:
extern int x; // declaration
int x = 10; // definition
Declaration → "this exists" Definition → "this is what it is"
30. What is the One Definition Rule?
The One Definition Rule, or ODR, specifies restrictions on how many definitions entities can have in a C++ program.
Generally:
- an entity requiring a definition must have one definition where required
- an entity generally cannot have multiple conflicting definitions in a program
- some entities such as templates and
inlinefunctions/classes can have identical definitions in multiple translation units under specific ODR rules
Example problem
// header.h
int x = 10;
If this header is included in multiple .cpp files, each translation unit may contain
a definition of x, potentially causing a linker error.
Better:
// header.h
extern int x;
// source.cpp
int x = 10;
inline does not simply mean "compiler should
inline this function." It also has important ODR/linkage
implications.
31. Static linking vs dynamic linking
Static linking
Library code is incorporated into the executable.
program + static library ↓ executable
Advantages:
- fewer runtime library dependencies
- self-contained executable
Disadvantages:
- larger executable
- updating the library generally requires relinking/rebuilding
Dynamic linking
The executable uses shared libraries loaded at runtime or through the platform's dynamic-linking mechanism.
program ↓ shared library
Advantages:
- libraries can be shared among processes
- library updates can sometimes happen independently
Disadvantages:
- runtime dependency/version issues
- deployment can be more complicated
32. What is a translation unit?
A translation unit is the source file after preprocessing, including all headers and expanded preprocessing directives that belong to that source file.
Conceptually:
main.cpp + #include files + macros/preprocessing ↓ Translation Unit ↓ Compiler ↓ Object file
If you have:
main.cpp
utils.cpp
you generally have two separate translation units.
Advanced Language
33. What is perfect forwarding?
Perfect forwarding allows a function template to pass an argument to another
function while preserving the argument's original value category and cv/ref properties
as appropriate.
Typical pattern:
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
Without std::forward, the named variable arg is an lvalue
expression, even if the caller passed an rvalue.
34. What is std::forward?
std::forward conditionally casts a forwarding-reference parameter back to the
value category it had at the call site.
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
If caller passes:
std::string s;
wrapper(s);
T is deduced as:
std::string&
If caller passes:
wrapper(std::string("hello"));
T is deduced as:
std::string
std::forward<T>(arg) preserves the corresponding lvalue/rvalue category.
std::move
→ unconditionally casts toward an rvalue/xvalue
std::forward
→ conditionally preserves the caller's value category
35. What is a universal/forwarding reference?
A forwarding reference is typically a function-template parameter of the form
T&& where T is deduced and the reference is not subject to certain cv constraints.
template <typename T>
void foo(T&& value) {
}
It can accept both lvalues and rvalues.
Example:
int x = 10;
foo(x); // lvalue
foo(10); // rvalue
"Universal reference" is older terminology popularized by Scott Meyers.
The standard terminology is forwarding reference.
36. What is template type deduction?
Consider:
template <typename T>
void foo(T&& x);
For an lvalue:
int a = 10;
foo(a);
T becomes:
int&
Then:
T&&
→ int& &&
which collapses to:
int&
For an rvalue:
foo(10);
T becomes:
int
Therefore:
T&& → int&&
This is the basis of perfect forwarding.
37. What is reference collapsing?
Reference collapsing defines what happens when references are combined.
The four important rules are:
T& & → T& T& && → T& T&& & → T& T&& && → T&&
If there is an &, the result is generally an lvalue reference.
Only:
&& + && → &&
remains an rvalue reference.
Example
template <typename T>
void foo(T&& x);
If:
int a = 10;
foo(a);
then:
T = int&
T&& = int& &&
= int&
38. What is SFINAE?
SFINAE means Substitution Failure Is Not An Error.
During template substitution, if a candidate becomes invalid in a context where
SFINAE applies, that candidate is removed from consideration instead of causing a
compilation error.
Classic example:
template <typename T>
typename
std::enable_if<std::is_integral_v<T>>::type
process(T value) {
std::cout << "Integer";
}
This overload participates only when T is integral.
Why important?
Before C++20 concepts, SFINAE was widely used to constrain templates.
SFINAE doesn't mean all template errors are silently ignored. It applies in
specific substitution contexts.
39. What are concepts?
Concepts are a C++20 feature used to specify constraints on template parameters.
Example:
template <typename T>
requires std::integral<T>
T add(T a, T b) {
return a + b;
}
Or:
template <std::integral T>
T add(T a, T b) {
return a + b;
}
Why better than many SFINAE techniques?
Concepts provide:
- clearer syntax
- better error messages
explicitconstraints- easier-to-read generic code
SFINAE
→ older template constraint technique
Concepts
→ modern C++20 constraint mechanism
40. What is CRTP?
CRTP, or Curiously Recurring Template Pattern, is a technique where a
class derives from a template instantiated with the derived
class itself.
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived\n";
}
};
Why use it?
CRTP can provide:
- compile-time polymorphism
staticdispatch- code reuse
- avoiding
virtual-function overhead in suitable designs
CRTP is different from traditional runtime polymorphism:
Virtual functions → runtime dispatch CRTP → compile-time/static dispatch
41. What is friend?
friend allows a specified function or class to access
private and protected members of another class.
Example:
class Person {
private:
int age = 20;
friend void showAge(const Person&);
};
void showAge(const Person& p) {
std::cout << p.age;
}
Friendship:
- is granted by the
class - is not inherited
- is not automatically mutual
- should be used carefully because it increases coupling
42. What is mutable?
mutable allows a non-static data member to be modified even when
the containing object is accessed through a const member function or
const object.
Example:
class Cache {
mutable int calls = 0;
public:
int get() const {
++calls;
return 10;
}
};
A const object can call get() and calls can still change.
Common use
Logical constness:
Object's externally observable state
→ remains logically const
Internal bookkeeping
→ can change
43. What is volatile?
volatile tells the compiler that an object's value may change for reasons
outside the normal flow of the program, so accesses should not be optimized away as ordinary
memory accesses.
Example:
volatile int hardwareRegister;
It is relevant to certain:
- memory-mapped hardware
- signal-handler-related scenarios
- low-level systems code
volatile does not provide thread synchronization and does
not make operations atomic.
Wrong:
volatile int counter;
and assuming multiple threads can safely increment it.
For thread synchronization, use:
std::atomic<int>
or an appropriate mutex.
volatile
→ special memory-access semantics
atomic
→ thread-safe atomic operations / synchronization primitives
44. What is static_assert?
static_assert checks a condition at compile time and produces a compilation error if the condition is false.
static_assert(sizeof(int) >= 4,
"int must be at least 4 bytes");
With templates
template <typename T>
void process(T value) {
static_assert(std::is_integral_v<T>,
"T must be integral");
}
If the condition is false, compilation fails.
Difference from normal assert
assert()
→ runtime check
static_assert()
→ compile-time check
C++ Coding Interview Patterns
This set is different from Sets 1–10. Here, the focus is writing C++ code in an SDE interview, implementing data structures, using STL correctly, and predicting code output.
For each question, I’ll use:
Simple explanation
C++ code
Part A — STL-Based Coding
1. Frequency counting using unordered_map
Interview answer:
Use unordered_map to store each value as a key and its frequency as the value.
vector<int> nums = {1, 2, 2, 3, 1, 2};
unordered_map<int, int> freq;
for (int x : nums) {
freq[x]++;
}
For the example:
1 -> 2
2 -> 3
3 -> 1
Complexity: Average O(n) time, O(k) space.
Common trap: unordered_map has average O(1) lookup, but worst-case
can be O(n).
2. Sorting using a custom comparator
Interview answer:
Pass a comparator to sort() to define the ordering.
vector<int> nums = {5, 2, 8, 1};
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
8 5 2 1
The comparator should define a strict weak ordering.
Trap: Don't write a >= b. Use a > b.
3. Sorting a vector of objects
struct Student {
string name;
int marks;
};
vector<Student> students = {
{"A", 80},
{"B", 95},
{"C", 70}
};
sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.marks > b.marks;
});
This sorts students by marks in descending order.
Trap: Pass objects as const& to avoid unnecessary copies.
4. Custom hash
Interview answer: A custom hash allows user-defined types to be used as keys in unordered containers.
Example:
struct Point {
int x, y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
struct PointHash {
size_t operator()(const Point& p) const {
return hash<int>{}(p.x) ^
(hash<int>{}(p.y) << 1);
}
};
unordered_map<Point, string, PointHash> mp;
mp[{1, 2}] = "A";
Important: If two keys compare equal using operator==, they must produce the same hash.
5. Priority queue with custom comparator
For a min-heap:
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(5);
pq.push(2);
pq.push(8);
cout << pq.top(); // 2
For objects:
struct Node {
int value;
};
struct Compare {
bool operator()(const Node& a, const Node& b) const {
return a.value > b.value;
}
};
priority_queue<Node, vector<Node>, Compare> pq;
Trap: priority_queue is a max-heap by default.
6. Set-based duplicate removal
Interview answer: Insert elements into a set when you need unique sorted values.
vector<int> nums = {4, 2, 4, 1, 2};
set<int> s(nums.begin(), nums.end());
for (int x : s)
cout << x << " ";
1 2 4
If ordering isn't needed:
unordered_set<int> s(nums.begin(), nums.end());
Complexity:
- set: O(n log n)
unordered_set: average O(n)
7. Map-based ordered processing
map automatically keeps keys sorted.
map<int, string> mp;
mp[3] = "C";
mp[1] = "A";
mp[2] = "B";
for (auto& [key, value] : mp) {
cout << key << " " << value << '\n';
}
1 A
2 B
3 C
Trap: unordered_map does not guarantee this ordering.
8. Two-pointer using iterators
Example: check whether a sorted vector contains two values with target sum.
vector<int> nums = {1, 2, 4, 7, 11};
int target = 9;
auto left = nums.begin();
auto right = nums.end() - 1;
while (left < right) {
int sum = *left + *right;
if (sum == target) {
cout << "Found";
break;
}
else if (sum < target) {
++left;
}
else {
--right;
}
}
Complexity: O(n).
Trap: This approach requires sorted data.
9. Binary search using STL
Use binary_search() when you only need to know whether an element exists.
vector<int> nums = {1, 3, 5, 7, 9};
bool found = binary_search(nums.begin(), nums.end(), 5);
For the position:
auto it = lower_bound(nums.begin(), nums.end(), 5);
if (it != nums.end() && *it == 5) {
cout << "Found";
}
genui{"learning_viz":{"type_id":"BINARY_SEARCH","initial_values":{"target":55}}}
Complexity: O(log n) for random-access iterators.
Trap: The range must be sorted for these binary-search algorithms.
10. Sliding window with unordered_map
Example: longest substring without repeating characters.
string s = "abcabcbb";
unordered_map<char, int> last;
int left = 0;
int ans = 0;
for (int right = 0; right < s.size(); right++) {
if (last.count(s[right])) {
left = max(left, last[s[right]] + 1);
}
last[s[right]] = right;
ans = max(ans, right - left + 1);
}
3
Complexity: Average O(n) time, O(k) space.
Trap: When moving left, don't move it backward. That's why
max() is important.
Part B — C++ Implementation Questions
11. Implement a dynamic array
A simplified dynamic array needs:
- pointer to allocated memory
- current size
- capacity
- resizing when full
class DynamicArray {
int* data;
size_t sz;
size_t cap;
void grow() {
size_t newCap = cap * 2;
int* newData = new int[newCap];
for (size_t i = 0; i < sz; i++)
newData[i] = data[i];
delete[] data;
data = newData;
cap = newCap;
}
public:
DynamicArray() : data(new int[2]), sz(0), cap(2) {}
void push_back(int x) {
if (sz == cap)
grow();
data[sz++] = x;
}
int& operator[](size_t i) {
return data[i];
}
size_t size() const {
return sz;
}
~DynamicArray() {
delete[] data;
}
};
Interviewer may ask: What is push_back() complexity?
Answer: Amortized O(1), because resizing happens occasionally.
Trap: A real vector also handles constructors, exceptions, allocators, iterators, move semantics, etc.
12. Implement a stack
class Stack {
vector<int> data;
public:
void push(int x) {
data.push_back(x);
}
void pop() {
if (!data.empty())
data.pop_back();
}
int top() const {
return data.back();
}
bool empty() const {
return data.empty();
}
};
Complexity:
- push: amortized O(1)
- pop: O(1)
- top: O(1)
13. Implement a queue
Using deque:
class Queue {
deque<int> q;
public:
void push(int x) {
q.push_back(x);
}
void pop() {
if (!q.empty())
q.pop_front();
}
int front() const {
return q.front();
}
bool empty() const {
return q.empty();
}
};
Trap: Don't implement a queue with a vector and repeatedly
erase(begin()); that makes removal O(n).
14. Implement a linked list
class LinkedList {
struct Node {
int data;
Node* next;
Node(int x) : data(x), next(nullptr) {}
};
Node* head = nullptr;
public:
void push_front(int x) {
Node* node = new Node(x);
node->next = head;
head = node;
}
void print() const {
Node* cur = head;
while (cur) {
cout << cur->data << " ";
cur = cur->next;
}
}
~LinkedList() {
while (head) {
Node* temp = head;
head = head->next;
delete temp;
}
}
};
Trap: Always release every dynamically allocated node.
15. Implement a doubly linked list
struct Node {
int data;
Node* prev;
Node* next;
Node(int x)
: data(x), prev(nullptr), next(nullptr) {}
};
Insertion at front:
void push_front(Node*& head, int x) {
Node* node = new Node(x);
node->next = head;
if (head)
head->prev = node;
head = node;
}
The advantage is that each node can move in both directions.
Trap: When deleting/inserting, update both prev and next.
16. Implement a circular queue
The important idea is:
rear = (rear + 1) % capacity
front = (front + 1) % capacity
genui{"learning_viz":{"type_id":"ARRAY_QUEUE_FRONT_REAR","initial_values":{"mode":"circular"}}}
A simple implementation:
class CircularQueue {
vector<int> q;
int front = 0;
int rear = 0;
int count = 0;
public:
CircularQueue(int capacity) : q(capacity) {}
bool push(int x) {
if (count == q.size())
return false;
q[rear] = x;
rear = (rear + 1) % q.size();
count++;
return true;
}
bool pop() {
if (count == 0)
return false;
front = (front + 1) % q.size();
count--;
return true;
}
};
Complexity: O(1) enqueue/dequeue.
17. Implement a hash table
For interview purposes, the basic idea is array + hash function + collision handling.
Using separate chaining:
class HashTable {
static const int SIZE = 10;
vector<pair<int, string>> table[SIZE];
int hashFunction(int key) {
return key % SIZE;
}
public:
void insert(int key, string value) {
int index = hashFunction(key);
for (auto& p : table[index]) {
if (p.first == key) {
p.second = value;
return;
}
}
table[index].push_back({key, value});
}
string* find(int key) {
int index = hashFunction(key);
for (auto& p : table[index]) {
if (p.first == key)
return &p.second;
}
return nullptr;
}
};
Collision is handled using separate chaining.
What happens if many keys go into the same bucket?
Lookup can degrade toward O(n).
18. Implement a simple unique_ptr
A simplified educational implementation:
template <typename T>
class SimpleUniquePtr {
T* ptr;
public:
explicit SimpleUniquePtr(T* p = nullptr)
: ptr(p) {}
~SimpleUniquePtr() {
delete ptr;
}
SimpleUniquePtr(const SimpleUniquePtr&) = delete;
SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete;
SimpleUniquePtr(SimpleUniquePtr&& other) noexcept
: ptr(other.ptr) {
other.ptr = nullptr;
}
SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
T& operator*() const {
return *ptr;
}
T* operator->() const {
return ptr;
}
};
Core interview idea:
unique ownership
↓
non-copyable
↓
movable
↓
destructor releases resource
Trap: A production smart pointer needs considerably more detail, including deleters and correct ownership semantics.
19. Implement reference counting
A simplified reference-counted pointer:
template <typename T>
class RefPtr {
T* ptr;
int* count;
public:
explicit RefPtr(T* p = nullptr)
: ptr(p), count(new int(1)) {}
RefPtr(const RefPtr& other)
: ptr(other.ptr), count(other.count) {
++(*count);
}
~RefPtr() {
release();
}
void release() {
if (--(*count) == 0) {
delete ptr;
delete count;
}
}
};
Important: This is educational, not production-quality.
A real shared_ptr has a control block and must handle assignment, moves, weak
references, thread-safety of control-block operations, custom deleters, etc.
Trap: Reference counting alone doesn't solve cycles.
20. Implement a thread-safe singleton
Modern C++ approach:
class Singleton {
private:
Singleton() = default;
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
Since C++11, initialization of a function-local static is thread-safe.
Trap: Don't add a mutex unnecessarily for this basic
initialization pattern.
21. Implement an LRU cache
Interview answer: Use:
- list for maintaining recency order
unordered_mapfor O(1) lookup
unordered_map
key → list iterator
list
Implementation:
class LRUCache {
int capacity;
list<pair<int, int>> items;
unordered_map<int, list<pair<int, int>>::iterator> mp;
public:
LRUCache(int capacity)
: capacity(capacity) {}
int get(int key) {
if (!mp.count(key))
return -1;
auto it = mp[key];
int value = it->second;
items.splice(items.begin(), items, it);
return value;
}
void put(int key, int value) {
if (mp.count(key)) {
auto it = mp[key];
it->second = value;
items.splice(items.begin(), items, it);
return;
}
if (items.size() == capacity) {
auto last = items.back();
mp.erase(last.first);
items.pop_back();
}
items.push_front({key, value});
mp[key] = items.begin();
}
};
Complexity:
- get: average O(1)
- put: average O(1)
Very common interview question: Why list?
Because once we have an iterator, moving/removing a list node is O(1).
22. Implement a custom comparator
For a struct:
struct Employee {
string name;
int salary;
};
struct Compare {
bool operator()(const Employee& a,
const Employee& b) const {
return a.salary < b.salary;
}
};
Usage:
sort(employees.begin(), employees.end(), Compare{});
Trap: Comparator logic depends on the algorithm/container. Always understand what true means for that particular API.
23. Implement a custom hash function
Example for a pair:
struct PairHash {
size_t operator()(const pair<int, int>& p) const {
size_t h1 = hash<int>{}(p.first);
size_t h2 = hash<int>{}(p.second);
return h1 ^ (h2 << 1);
}
};
Usage:
unordered_map<pair<int, int>, int, PairHash> mp;
Trap: Equal keys must have equal hashes.
24. Implement a min heap/max heap
Using a vector, a binary heap uses:
parent = (i - 1) / 2
left = 2 * i + 1
right = 2 * i + 2
For a max heap:
class MaxHeap {
vector<int> heap;
public:
void push(int x) {
heap.push_back(x);
int i = heap.size() - 1;
while (i > 0) {
int parent = (i - 1) / 2;
if (heap[parent] >= heap[i])
break;
swap(heap[parent], heap[i]);
i = parent;
}
}
int top() const {
return heap.front();
}
void pop() {
if (heap.empty())
return;
heap[0] = heap.back();
heap.pop_back();
int i = 0;
while (true) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < heap.size() &&
heap[left] > heap[largest])
largest = left;
if (right < heap.size() &&
heap[right] > heap[largest])
largest = right;
if (largest == i)
break;
swap(heap[i], heap[largest]);
i = largest;
}
}
};
Complexity:
- top: O(1)
- push: O(log n)
- pop: O(log n)
25. Implement producer-consumer
Core tools:
mutex
condition_variable
queue
Producer:
queue<int> q;
mutex mtx;
condition_variable cv;
void producer() {
for (int i = 0; i < 5; i++) {
{
lock_guard<mutex> lock(mtx);
q.push(i);
}
cv.notify_one();
}
}
Consumer:
void consumer() {
while (true) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [] {
return !q.empty();
});
int value = q.front();
q.pop();
lock.unlock();
cout << value << '\n';
}
}
Why predicate?
Because condition-variable waits can have spurious wakeups.
Trap: Don't access the shared queue without synchronization.
Part C — C++ Code-Output Questions
These are especially important because interviewers often give a short snippet and ask:
"What will this print, and why?"
26. Constructor/destructor output
class A {
public:
A() {
cout << "Constructor\n";
}
~A() {
cout << "Destructor\n";
}
};
int main() {
A obj;
}
Constructor
Destructor
Constructor runs when object is created.
Destructor runs when the object leaves its lifetime.
Trap: Destruction is reverse order of construction.
27. Copy constructor output
class A {
public:
A() {
cout << "Default\n";
}
A(const A&) {
cout << "Copy\n";
}
};
int main() {
A a;
A b = a;
}
Default
Copy
A b = a initializes a new object from an existing object, so the copy constructor is selected.
b = a;
after b already exists is copy assignment, not copy construction.
28. Move constructor output
class A {
public:
A() {
cout << "Default\n";
}
A(const A&) {
cout << "Copy\n";
}
A(A&&) noexcept {
cout << "Move\n";
}
};
int main() {
A a;
A b = std::move(a);
}
Default
Move
std::move(a) allows the move constructor to be selected.
Trap: std::move() itself does not move anything. It converts the
expression to an xvalue.
29. Virtual function output
class Base {
public:
virtual void show() {
cout << "Base\n";
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived\n";
}
};
int main() {
Base* ptr = new Derived;
ptr->show();
delete ptr;
}
Derived
because show() is virtual and dispatch occurs based on the dynamic
object type.
Trap: If Base has resources and is intended to be deleted polymorphically, its
destructor should generally be virtual.
30. Slicing output
class Base {
public:
virtual void show() {
cout << "Base\n";
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived\n";
}
};
int main() {
Derived d;
Base b = d;
b.show();
}
Base
Why?
Derived object
↓ copy by value
Base object
↓
Derived part sliced away
This is object slicing.
31. Static variable output
class Counter {
public:
static int count;
Counter() {
++count;
}
};
int Counter::count = 0;
int main() {
Counter a;
Counter b;
Counter c;
cout << Counter::count;
}
3
count belongs to the class, not to each object.
Trap: A static data member has one shared value across objects.
32. ++i vs i++
int i = 5;
cout << ++i;
6
Pre-increment:
increment ↓ return new value
Post-increment:
int i = 5;
cout << i++;
5
Afterward:
i == 6
Interview answer: ++i increments first and yields the new value; i++ yields the old value and then increments.
33. Pointer arithmetic
int arr[] = {10, 20, 30, 40};
int* p = arr;
cout << *(p + 2);
30
p + 2 advances by two int elements, not two bytes.
genui{"learning_viz":{"type_id":"C_ARRAY_POINTER_ARITHMETIC","initial_values":{"index":2}}}
Therefore:
arr[2]
and
*(arr + 2)
refer to the same element.
Trap: Pointer arithmetic is scaled by the pointed-to type.
34. Reference behavior
int x = 10;
int& ref = x;
ref = 20;
cout << x;
20
A reference is an alias for an existing object.
Trap: A reference cannot later be reseated to refer to another object.
35. const behavior
const int x = 10;
// x = 20; // Error
A const object cannot be modified through that const access.
With a pointer:
const int* p = &x;
means:
pointer to const int
You cannot modify *p through p.
But:
int* const p = &x;
means:
const pointer
The pointer itself cannot point somewhere else.
Trap: const int* and int*
const mean different things.
36. sizeof questions
int arr[5];
cout << sizeof(arr);
If sizeof(int) == 4, output:
20
Because:
5 × 4 = 20
But:
void func(int arr[]) {
cout << sizeof(arr);
}
Inside the function, arr is adjusted to a pointer parameter, so sizeof(arr) gives
the pointer size, not the original array size.
Trap: Arrays and pointers are related, but they are not the same type.
37. Temporary object lifetime
class A {
public:
A() {
cout << "A\n";
}
~A() {
cout << "~A\n";
}
};
int main() {
A();
cout << "End\n";
}
The temporary object is destroyed at the end of the full-expression, so conceptually:
A
~A
End
Trap: Temporary lifetime rules depend on context; binding a temporary to certain references can extend its lifetime.
38. Lambda capture output
int x = 10;
auto f = [x]() mutable {
x++;
cout << x << '\n';
};
f();
cout << x;
11
10
The lambda captured its own copy of x.
mutable allows modification of that captured copy.
Compare:
auto f = [&x]() {
x++;
};
Now the original x changes.
Trap: [x] and [&x] have very different lifetime and modification behavior.
39. shared_ptr reference count
auto p1 = make_shared<int>(10);
cout << p1.use_count() << '\n';
{
auto p2 = p1;
cout << p1.use_count() << '\n';
}
cout << p1.use_count();
1
2
1
Copying shared_ptr increases the strong reference count.
Destroying p2 decreases it.
Trap: weak_ptr does not increase the strong count.
40. Iterator invalidation
One of the most important STL traps:
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4);
If push_back() causes reallocation, it becomes invalid.
Why?
old memory [1][2][3] ↓ reallocation new memory [1][2][3][4]
The old elements were moved/copied to another storage location.
For vector, reallocation invalidates:
- iterators
- pointers
- references
to its elements.
Important: If no reallocation occurs, push_back() still invalidates
iterators at/after the insertion position and the past-the-end iterator, while existing element
references/pointers remain valid. For interview answers, always distinguish
reallocation from insertion without reallocation.
Most Important Interview Patterns
For an SDE interview, memorize these combinations:
| Problem | Typical C++ tool |
|---|---|
| Frequency | unordered_map |
| Unique values | set / unordered_set |
| Sorted key processing | map |
| Top K | priority_queue |
| Custom ordering | Comparator |
| Custom object as hash key | Custom hash + operator== |
| Two Sum | unordered_map |
| Two pointers | Sorted vector + pointers/iterators |
| Binary search | lower_bound / upper_bound |
| Sliding window | unordered_map + two pointers |
| LRU Cache | list + unordered_map |
| Dynamic array | Dynamic allocation + resizing |
| Stack | vector |
| Queue | deque |
| Circular queue | Array + modulo |
| Hash table | Buckets + collision handling |
| Exclusive ownership | unique_ptr |
| Shared ownership | shared_ptr |
| Producer-consumer | mutex + condition_variable + queue |