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:

C++
print();

Which print() should be called?

Namespaces allow us to separate them:

C++
namespace A {
    void print() {
        cout << "A";
    }
}

namespace B {
    void print() {
        cout << "B";
    }
}

Now:

C++
A::print();
B::print();

The :: operator is called the scope resolution operator.

Code example

C++
#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;
}
Output:
Output
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::

C++
using namespace std;

vector<int> nums;
cout << "Hello";

instead of:

C++
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:

C++
int age = 21;
double salary = 50000.5;
char grade = 'A';
bool passed = true;

2. Derived types

Examples:

C++
int arr[5];
int* ptr;
int& ref = x;

Common derived types:

  • Arrays
  • Pointers
  • References
  • Function types

3. User-defined types

Examples:

C++
class Student {};
struct Employee {};

Also:

C++
union Data {};

4. Enumeration

C++
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 are private by default. In a struct, all members are public by default.
  • Inheritance Access: Inheritance defaults to private for classes and public for structs.
  • Convention: Developers use struct for plain data records and class when writing complex systems with private states 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 struct creates separate slots: It takes 12 bytes (4 + 8) plus alignment padding.
  • A union overlays the slots: It takes only 8 bytes (the size of the double). 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.

C++
#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:

C++
class Car {
public:
    string brand;

    void drive() {
        cout << "Car is driving\n";
    }
};

Create objects:

C++
Car car1;
Car car2;

Both objects belong to the same class but can have different state:

C++
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.

Common trap / wrong answer

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.

C++
class A {
public:
    int x;
};

private

Accessible only from within the class and its friends.

C++
class A {
private:
    int x;
};

protected

Accessible from:

  • The class itself
  • Friend declarations
  • Derived classes
C++
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?

C++
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

C++
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:

C++
account.balance = -1000;

because balance is private.

Instead, it must use:

C++
account.deposit(1000);

Why is encapsulation useful?

It provides:

  • Data protection
  • Controlled modification
  • Better maintainability
  • Clear interfaces
  • Reduced coupling
Common trap / wrong answer

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:

C++
withdraw();
deposit();
checkBalance();

But you don't need to know how the bank internally processes the transaction.

Simple example

Consider:

C++
class Car {
public:
    void start() {
        // Complex internal operations
        // fuel system
        // ignition
        // engine control
        // etc.
    }
};

The user only needs:

C++
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 class contains at least one pure virtual function.
  • Objects of an abstract class cannot be instantiated.
  • Derived classes must implement the pure virtual functions.
  • It provides a common interface to achieve abstraction.

In C++, abstraction can be achieved using abstract classes and pure virtual functions.

C++
#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

C++
class Shape {
public:
    virtual double area() const = 0;

    virtual ~Shape() = default;
};

Shape defines what derived classes must provide without defining the complete implementation.

C++
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
Common trap / wrong answer

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:

C++
int add(int a, int b) {
    return a + b;
}

Calling:

C++
int result = add(10, 20);

What is the difference between a function declaration and definition?

Declaration:

C++
int add(int, int);

Definition:

C++
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

C++
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:

C++
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:

C++
int getValue();
double getValue();

The compiler cannot distinguish functions based only on return type.

Common trap / wrong answer

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

C++
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:

C++
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

Common trap / wrong answer

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

C++
inline int square(int x) {
    return x * x;
}

Calling:

C++
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.

Common trap / wrong answer

❌:

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

C++
void counter() {
    static int count = 0;
    count++;

    cout << count << '\n';
}

Calling:

C++
counter();
counter();
counter();

outputs:

1

2

3

The variable retains its value between calls.

Static class member

C++
class Student {
public:
    static int count;

    Student() {
        count++;
    }
};

int Student::count = 0;

If we create:

C++
Student s1;
Student s2;
Student s3;

there is one shared count, not three separate count variables.

Static member function

C++
class Student {
public:
    static int count;

    static void showCount() {
        cout << count;
    }
};

Call:

C++
Student::showCount();

No object is required.

Important limitation

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.

C++
class A {
    int x;

public:
    static void fun() {
        // cout << x;  // Error
    }
};

There is no particular object from which to obtain x.

Common trap / wrong answer

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

C++
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.

Important point

A friend function:

  • Is not a member function
  • Does not have a this pointer
  • Can access private/protected members
  • 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.

Common trap / wrong answer

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

C++
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:

C++
class A {
    friend class B;
};

then B can access A's private members.

It does not mean A can access B's private members.

Common trap / wrong answer

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

C++
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

C++
class Student {
private:
    int age;

public:
    Student& setAge(int age) {
        this->age = age;
        return *this;
    }
};

Then:

C++
Student s;

s.setAge(20).setAge(21);
Important point

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:

C++
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.

Common trap / wrong answer

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

C++
int x = 10;

int& ref = x;

ref = 20;

cout << x;
Output:
Output
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:

C++
int x = 10;
int y = 20;

int* p = &x;
p = &y;

Reference:

C++
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:

C++
int* arr[5];

but not:

C++
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

C++
void increment(int x) {
    x++;
}

int main() {
    int n = 10;

    increment(n);

    cout << n;
}
Output:
Output
10

Because x is a copy.

Pass by reference

C++
void increment(int& x) {
    x++;
}

int main() {
    int n = 10;

    increment(n);

    cout << n;
}
Output:
Output
11

For read-only large objects

A very common C++ pattern is:

C++
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.

Common trap / wrong answer

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

C++
int x = 10;

int* p = &x;

cout << *p;
Output:
Output
10

Here:

&x → address of x
p  → stores that address
*p → accesses the object at that address

Pointer can be changed

C++
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++:

C++
int* p = nullptr;
Common trap / wrong answer

Don't dereference:

C++
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

C++
int* p = nullptr;

if (p == nullptr) {
    cout << "Null pointer";
}

Why is nullptr better than NULL?

Consider overloaded functions:

C++
void fun(int);
void fun(int*);

With:

C++
fun(NULL);

depending on how NULL is defined, the call can resolve to the integer overload.

But:

C++
fun(nullptr);

selects the pointer overload.

What is the type of nullptr?

Conceptually:

std::nullptr_t

Common trap / wrong answer

❌ 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

C++
const int x = 10;

// x = 20; // Error

Pointer to const

C++
const int* p;

You cannot modify the integer through p.

C++
*p = 20; // Error

But p itself can point somewhere else.

Const pointer

C++
int* const p = &x;

The pointer cannot point somewhere else, but the pointed object can be modified through p.

Both const

C++
const int* const p = &x;

Neither the pointer nor the pointed value can be modified through p.

Const member function

C++
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:

C++
void print(const string& name);

It means:

  • No unnecessary copy
  • Function cannot modify name

This is a very common and important C++ pattern.

Common trap / wrong answer

Remember the difference:

C++
const int* p;

means pointer to const int.

C++
int* const p;

means const pointer to int.

What is a pointer to const?

C++
const int x = 10;

const int* p = &x;

or:

C++
int x = 10;

const int* p = &x;

You cannot modify x through p:

C++
*p = 20;  // ❌

But p itself can point somewhere else:

C++
int y = 20;

p = &y;   // ✅

What is a const pointer?

C++
int x = 10;
int y = 20;

int* const p = &x;

Here the pointer itself is constant.

So:

C++
*p = 30;  // ✅

but:

C++
p = &y;   // ❌

What is a const pointer to const data?

C++
const int* const p = &x;

Now neither can change through p:

C++
*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:

C++
class Account {
    double balance;

public:
    double getBalance() const {
        return balance;
    }

    void deposit(double amount) {
        balance += amount;
    }
};

Then:

C++
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.

C++
void greet(string name = "Dilip") {
    cout << "Hello " << name;
}

greet();          // Hello Dilip
greet("Rahul");   // Hello Rahul

If the argument is not passed, "Dilip" is used.

Important rules:

Default arguments should generally be given from right to left.

C++
void fun(int a, int b = 10, int c = 20);  // Valid

But:

C++
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.

C++
// Declaration
void print(int x, int y = 10);

// Definition
void print(int x, int y) {
    cout << x << " " << y;
}

Then:

C++
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:

  1. Local/block scope
  2. Function parameter scope
  3. Class scope
  4. Namespace scope
  5. Global scope

Example:

C++
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:

C++
void fun() {
    int x = 10;
}

x:

  • Scope → inside fun()
  • Lifetime → from its initialization until fun() exits
Important interview point:

Scope and lifetime are related but not the same thing.

A static local variable is a good example:

C++
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.

C++
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:

C++
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.
What interviewer may ask next:

Can a constructor return a value?

Answer: No. Constructors do not have a return type.

Common trap:

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:

C++
class Account {
    int balance;
};

We could accidentally have an uninitialized balance for a default-initialized object.

With a constructor:

C++
class Account {
    int balance;

public:
    Account(int initialBalance) : balance(initialBalance) {}
};

Now:

C++
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:

C++
class Student {
public:
    Student() {
        cout << "Default constructor";
    }
};

Student s;

It can also have default parameters:

C++
class Student {
public:
    Student(int age = 18) {}
};

Student s;  // Valid
Important distinction

A default constructor does not necessarily mean a constructor literally written as ClassName().

This is also a default constructor:

C++
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.

C++
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.

C++
class Student {
public:
    Student() {}

    Student(int age) {}

    Student(string name, int age) {}
};

Now:

C++
Student a;
Student b(21);
Student c("Dilip", 21);

The compiler chooses the appropriate constructor based on the arguments.

What interviewer may ask next:

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:

C++
class Student {
public:
    Student(const Student& other) {
        // copy data
    }
};

Example:

C++
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.
Common trap:

Copy constructor:

C++
Student(const Student&);

Copy assignment:

C++
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:

C++
class Buffer {
public:
    Buffer(Buffer&& other) {
        // take ownership of other's resources
    }
};

Example:

C++
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.

C++
class Base {
public:
    virtual Base() {}   // ❌ Invalid
};
Important interview concept:

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.

C++
class Singleton {
private:
    Singleton() {}

public:
    static Singleton& getInstance() {
        static Singleton obj;
        return obj;
    }
};

External code cannot directly do:

C++
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.

C++
class Student {
    string name;
    int age;

public:
    Student() : Student("Unknown", 18) {}

    Student(string n, int a)
        : name(n), age(a) {}
};

Here:

C++
Student()

delegates to:

C++
Student(string, int)

Benefit:

Avoids duplicating initialization code.

What is an initializer list?

An initializer list initializes data members before the constructor body executes.

C++
class Student {
    string name;
    int age;

public:
    Student(string n, int a)
        : name(n), age(a) {
    }
};

The part:

C++
: 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:

C++
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:

C++
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:

C++
class A {
    int x;

public:
    A(int value) : x(value) {}
};

x is initialized.

Assignment:

C++
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.

Interview answer:

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:

C++
class Student {
public:
    ~Student() {
        cout << "Destructor called";
    }
};

For:

C++
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:

C++
~Student();
~Student(int x);  // ❌

There is only:

C++
~Student();

Can a destructor take parameters?

No. A destructor cannot take parameters and cannot have a return type.

C++
~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.

C++
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:

C++
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;
}
Output:
Output
Derived destructor
Base destructor

Without a virtual destructor, deleting a derived object through a base pointer results in undefined behavior.

Very common interview question:

C++
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:

C++
class A {};
class B : public A {};
class C : public B {};

Construction:

A → B → C

Destruction:

C → B → A

What is the base constructor vs derived constructor order?

The base class constructor always executes before the derived class constructor.

C++
class Base {
public:
    Base() {
        cout << "Base\n";
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived\n";
    }
};

Creating:

C++
Derived d;
Output:
Output
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.

C++
class Animal {
public:
    void eat() {
        cout << "Eating\n";
    }
};

class Dog : public Animal {
public:
    void bark() {
        cout << "Barking\n";
    }
};

Now:

C++
Dog d;

d.eat();
d.bark();

Dog inherits from Animal.

What are the types of inheritance?

The commonly discussed types are:

  1. Single inheritance
  2. Multiple inheritance
  3. Multilevel inheritance
  4. Hierarchical inheritance
  5. Hybrid inheritance

What is single inheritance?

One derived class inherits from one base class.

C++
class Animal {};

class Dog : public Animal {};
Animal
   ↓
 Dog

What is multiple inheritance?

A derived class inherits from multiple base classes.

C++
class Printer {};

class Scanner {};

class Machine : public Printer, public Scanner {};

Printer Scanner

C++
    \         /
     \       /
      Machine
Common issue:

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.

C++
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.

C++
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:

C++
add(2, 3);
add(2.5, 3.5);  // overloading

and:

C++
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:

C++
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.

C++
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

C++
void add(int a, int b);
void add(double a, double b);

Overriding

C++
class Base {
public:
    virtual void show() {}
};

class Derived : public Base {
public:
    void show() override {}
};
Common trap:

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

C++
#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();
}
Output:
Output
Derived

Why does it print Derived?

Look carefully at this line:

C++
Base& b = d;

Here:

  • b is a Base reference
  • The actual object is a Derived object

So we have:

Reference type Actual object

C++
     Base      --->     Derived

When we call:

C++
b.show();

because show() is virtual, C++ checks the actual object type at runtime.

The actual object is Derived, so:

C++
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:

C++
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();
}
Output:
Output
Base
Why?

Because show() is not virtual.

The compiler uses the type of the reference:

Base& b

Therefore:

C++
b.show();

calls:

C++
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:

C++
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:

C++
int main() {
    Dog dog;
    Cat cat;

    Animal* a1 = &dog;
    Animal* a2 = &cat;

    a1->sound();
    a2->sound();
}
Output:
Output
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:

C++
class Payment {
public:
    virtual void pay() = 0;
};

Then:

C++
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:

C++
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:

C++
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?

Derived object vptr other data Derived vtable &Derived::show Derived::show()
Figure: dynamic dispatch. The object stores a vptr, the vptr selects the vtable, and the vtable entry points to the function that runs.

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:

C++
Derived d;

conceptually:

d
│
│ vptr
↓
Derived vtable
│
↓
Derived::show()

How does this help with runtime polymorphism?

Consider:

C++
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:

C++
ptr->show();

calls:

C++
Derived::show();

Step-by-step execution

Suppose we have:

C++
class Base {
public:
    virtual void show() {
        cout << "Base";
    }
};

class Derived : public Base {
public:
    void show() override {
        cout << "Derived";
    }
};

And:

C++
Derived d;
Base* ptr = &d;

ptr->show();

Conceptually, the process is:

Step 1 — Create Derived object

C++
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

C++
Base* ptr = &d;

The pointer points to the same Derived object:

ptr
 |
 ↓
+----------------+
| vptr ----------|-----> Derived vtable
+----------------+          |
| Derived data   |          ↓
+----------------+     Derived::show()

Step 4 — Call the function

C++
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:

C++
virtual void show();

This enables dynamic dispatch.

Overriding

The derived class provides its own implementation:

C++
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.

C++
class Shape {
public:
    virtual double area() = 0;
};

This is invalid:

C++
Shape s;  // ❌

But this is valid:

C++
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.

C++
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:

C++
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.

C++
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.

C++
class Shape {
public:
    virtual void draw() = 0;
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Circle";
    }
};

Shape* ptr = new Circle();  // ✅
ptr->draw();

But:

C++
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:

C++
class Base {
public:
    Base() {
        show();
    }

    virtual void show() {
        cout << "Base";
    }
};

class Derived : public Base {
public:
    void show() override {
        cout << "Derived";
    }
};
Derived d;
Output:
Output
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.

Very important correction to a common misconception:

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:

C++
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

C++
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:

C++
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:

C++
Base& b = d;

or:

C++
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?

A B C D A B C D Without virtual: D has two A subobjectsWith virtual: D has one shared A
Figure: diamond inheritance. Arrows point from derived class to base class.

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:

C++
class A {
public:
    int x;
};

class B : public A {};
class C : public A {};

class D : public B, public C {};

Structure:

C++
       A
      / \
     B   C
      \ /
       D

D contains two copies of A.

So:

C++
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.

C++
class A {
public:
    int x;
};

class B : virtual public A {};
class C : virtual public A {};

class D : public B, public C {};

Now:

C++
       A
      / \

virtual virtual

C++
    B       C
      \     /
         D

D contains only one shared A subobject.

Therefore:

C++
D d;

d.x = 10;  // ✅
Important interview point:

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

C++
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

C++
class Teacher {};

class Department {
    Teacher* teacher;
};

The teacher can exist independently of the department.

Interview shortcut:
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

C++
class Dog : public Animal {};

A:

Dog IS-A Animal

Composition

C++
class Car {
    Engine engine;
};

A:

Car HAS-A Engine

Example:

Don't use inheritance just because two classes share some code.

Prefer:

C++
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:

C++
class Car : public Engine {};

This says:

Car IS-A Engine

which is conceptually wrong.

Instead:

C++
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.

C++
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-class problem
  • 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.

C++
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:

C++
int* p1 = NULL;
int* p2 = nullptr;

Prefer:

C++
int* p = nullptr;

Why?

Consider:

C++
void fun(int);
void fun(int*);

fun(NULL);

Depending on how NULL is defined, overload resolution can become problematic.

But:

C++
fun(nullptr);

clearly selects:

C++
fun(int*);

What is a dangling pointer?

A dangling pointer is a pointer that refers to an object whose lifetime has ended.

Example:

C++
int* p;

{
    int x = 10;
    p = &x;
}

// x no longer exists

cout << *p;  // ❌ Undefined behavior

Another example:

C++
int* p = new int(10);

delete p;

// p is dangling

Safer:

C++
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.

C++
int* p;
cout << *p;  // ❌ Undefined behavior

Initialize pointers:

C++
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.

C++
int x = 10;

void* p = &x;

int* ip = static_cast<int*>(p);

cout << *ip;

You cannot directly do:

C++
void* p = &x;

// cout << *p;  // ❌ Cannot dereference void*
Important:

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:

C++
int add(int a, int b) {
    return a + b;
}

int (*ptr)(int, int) = add;

cout << ptr(2, 3);  // 5

Here:

C++
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.

C++
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?

arr[0] = 10+0 bytes arr[1] = 20+4 bytes arr[2] = 30+8 bytes arr[3] = 40+12 bytesp p + 1 p + 2
Figure: for an int* with 4-byte int, p + 1 advances by one element (4 bytes), not one byte.

Pointer arithmetic allows moving a pointer across elements of an array or another contiguous sequence.

Example:

C++
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:

C++
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.

C++
int arr[3] = {10, 20, 30};

int (*p)[3] = &arr;

cout << (*p)[0];  // 10
cout << (*p)[1];  // 20

Notice the parentheses:

C++
int (*p)[3];

Without them:

C++
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:

C++
int a = 10;
int b = 20;

int& arr[2] = {a, b};  // ❌

Instead, use:

C++
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

StackHeap main() frame int x = 10 int* p = 0x1000 ... (grows down) 0x1000: 20(new int) other heap blocks
Figure: p lives on the stack; the object it points to lives on the 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:

C++
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:

C++
delete x;

because x wasn't dynamically allocated.

Why is it called "Stack"?

Function calls are typically managed using a call stack.

Suppose:

C++
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:

C++
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:

C++
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:

C++
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.

C++
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:

C++
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:

C++
std::vector<int> arr(n);

The vector manages its dynamic memory automatically.

Stack vs Heap Example

Consider:

C++
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:

C++
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:

C++
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:

C++
int* p = new int(20);

delete p;

prefer:

C++
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:

C++
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.

C++
int* p = new int(10);

Conceptually:

1. Allocate storage

2. Construct/initialize int with 10

3. Return its address

For a class:

C++
Student* s = new Student("Dilip");

new allocates storage and invokes the appropriate constructor.

What happens when delete is used?

C++
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:

C++
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:

C++
Student* s = new Student();
delete s;

versus:

C++
Student* s = static_cast<Student*>(malloc(sizeof(Student)));
free(s);

The second does not construct a proper Student object.

Interview recommendation:

In modern C++, generally prefer containers and smart pointers over manual new.

C++
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:

C++
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:

C++
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:

C++
int* arr = new int[5];

delete arr;  // ❌

Using delete on memory allocated with new[] results in undefined behavior.

Correct:

C++
delete[] arr;

Similarly:

C++
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:

C++
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:

C++
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.

C++
int* p = new int(10);

delete p;
delete p;  // ❌

Safer:

C++
delete p;
p = nullptr;

Then:

C++
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.

C++
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:

C++
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:

C++
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
Very important interview concept:

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:

C++
void fun() {
    Student s;
}

Conceptually:

s constructor
      ↓
object lifetime begins
      ↓
use object
      ↓
scope ends
      ↓
destructor
      ↓
object lifetime ends

For dynamically allocated objects:

C++
Student* p = new Student();

delete p;

The object's lifetime ends when it is destroyed by delete.

Important:

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++:

C++
int* p = new int(10);

delete p;

Modern C++:

C++
auto p = make_unique<int>(10);

Containers also dynamically allocate internally:

C++
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:

C++
#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:

C++
alignof(int)

and request aligned storage using:

C++
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:

C++
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:

C++
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:

C++
A arr[10];

have correctly aligned members.

Interview optimization question

Compare:

C++
struct A {
    char c;
    int x;
    char d;
};

with:

C++
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:

C++
int x = 10;

x = 20;

x is an lvalue.

Another example:

C++
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:

C++
int x = 10;

int y = x + 5;

x + 5 is an rvalue.

Also:

C++
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:

C++
int x = 10;

int y = x + 5;

x + 5 is an rvalue.

Also:

C++
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

C++
string("Hello")

For example:

C++
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.

C++
int x = 10;

int& ref = x;

Now:

C++
ref = 20;

cout << x;  // 20

It is an alias for x.

Normally:

C++
int& ref = 10;  // ❌

because a non-const lvalue reference cannot bind to that temporary.

But:

C++
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:

C++
int&& ref = 10;

And:

C++
string s = "Hello";

string&& ref = std::move(s);

A common use is a move constructor:

C++
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:

C++
class Student {
public:
    Student(const Student& other) {
        // copy data
    }
};

Example:

C++
Student s1;
Student s2 = s1;

Here s2 is a new object, so the copy constructor is used.

Common trap:
C++
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:

C++
Student s1;
Student s2;

s2 = s1;

This invokes:

operator=

Conceptually:

C++
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:

C++
Student a;

Student b = a;  // Copy constructor

Student c;
c = a;          // Copy assignment
Easy rule:

New object → constructor. Existing object → assignment.

When is a copy constructor called?

Common situations:

1. Initializing from another object

C++
Student a;
Student b = a;

2. Passing by value

C++
void fun(Student s);

Student a;
fun(a);

A copy may be made for the parameter.

3. Returning by value

C++
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

C++
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:

C++
class Buffer {
public:
    int* data;

    Buffer(int value) {
        data = new int(value);
    }
};

Suppose:

C++
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:

C++
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:

C++
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:

C++
class Buffer {
public:
    int* data;

    Buffer(int value) {
        data = new int(value);
    }

    ~Buffer() {
        delete data;
    }
};

If the compiler-generated copy simply copies the pointer:

C++
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.

C++
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:

C++
class Buffer {
public:
    int* data;

    Buffer(int value) {
        data = new int(value);
    }

    ~Buffer() {
        delete data;
    }
};

You didn't write :

C++
Buffer(const Buffer& other)

But C++ can provide an implicitly-declared copy constructor performs member-wise copying.

Conceptually, it behaves approximately like:

C++
Buffer(const Buffer& other) {
    data = other.data;
}

Notice:

C++
data = other.data;

It copies the pointer value.

It does NOT do:

C++
data = new int(*other.data);

Therefore:

C++
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:

C++
class Buffer {
public:
    int* data;

    Buffer(int value) {
        data = new int(value);
    }

    ~Buffer() {
        delete data;
    }
};

Now:

C++
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:

C++
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:

C++
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:

C++
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:

C++
a = a;

This is called self-assignment.

If our assignment operator blindly did:

C++
delete data;
data = new int(*other.data);

then after:

C++
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:

C++
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:

C++
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:

C++
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:

C++
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:

C++
Buffer(const Buffer& other);

It creates a new object by copying another object.

Example:

C++
Buffer b = a;

Copy assignment operator in Rule of 5

The copy assignment operator is:

C++
Buffer& operator=(const Buffer& other);

Example:

C++
b = a;

The object b already exists.

Destructor in Rule of 5

The destructor:

C++
~Buffer();

releases resources owned by the object.

For raw memory:

C++
~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:

C++
class Buffer {
    int* data;
};

prefer:

C++
class Buffer {
    vector<int> data;
};

or:

C++
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.

C++
Buffer(Buffer&& other) noexcept;

Example:

C++
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.

C++
Buffer& operator=(Buffer&& other) noexcept;

Example:

C++
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:

C++
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.

C++
string&& ref = string("Hello");

Also:

C++
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:

C++
string a = "Hello";

string b = std::move(a);

The important part is:

C++
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:

C++
Buffer b = a;            // copy
Buffer c = std::move(a); // move

Copy assignment vs Move assignment

Copy assignment

C++
b = a;

Conceptually:

duplicate a's resources

Move assignment

C++
b = std::move(a);

Conceptually:

release b's old resource

take a's resource

leave a valid

Typical signature:

C++
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:

C++
class Buffer {
public:
    Buffer(Buffer&& other) noexcept {
        // transfer resources
    }
};

Consider vector growth:

C++
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:

C++
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:

C++
string a = "Hello";
string b = std::move(a);

a = "New value";  // ✅
cout << a;        // New value

Avoid assuming:

C++
cout << a;

will necessarily print the old "Hello".

Key phrase:

Valid but unspecified state.

Why does vector prefer moving during reallocation when possible?

Suppose:

C++
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.

Interview answer:

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:

C++
class A {
public:
    A() {
        cout << "Constructor\n";
    }

    A(const A&) {
        cout << "Copy constructor\n";
    }

    A(A&&) {
        cout << "Move constructor\n";
    }
};

Now:

C++
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:

C++
A create() {
    return A();
}

And:

C++
A obj = create();

The important line is:

C++
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:

C++
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:

C++
A create() {
    A obj;
    return obj;
}

Here:

C++
A obj;

creates a named local object.

Then:

C++
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

C++
A create() {
    return A();
}

The returned object is a temporary/prvalue.

NRVO

C++
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:

C++
class File {
public:
    File() {
        // acquire file resource
    }

    ~File() {
        // release file resource
    }
};

Usage:

C++
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:

C++
resource = acquire();

if (error)
    return;  // Could forget cleanup

release(resource);

With RAII:

C++
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:

C++
int* p = new int(10);

and remembering:

C++
delete p;

use:

C++
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:

C++
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++.

Interview answer:

RAII ensures resources are released automatically during both normal scope exit and exception-driven stack unwinding.

RAII and mutexes

Instead of manually:

C++
mutex.lock();

// code

mutex.unlock();

use:

C++
lock_guard<mutex> lock(mutex);

When lock goes out of scope:

scope ends
   ↓
lock_guard destructor
   ↓
mutex unlocked

Example:

C++
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:

C++
#include <fstream>

void readFile() {
    ifstream file("data.txt");

    // use file

} // file automatically closes

You don't normally need:

C++
file.close();

just to ensure cleanup when the object goes out of scope.

Interview answer:

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:

C++
int* p = new int(10);

delete p;

If we forget delete, we can get a memory leak.

A smart pointer manages the lifetime automatically:

C++
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

C++
#include <memory>

int main() {
    std::unique_ptr<int> p = std::make_unique<int>(10);
}

No explicit delete is required.

Interviewer may ask

Why is automatic cleanup possible?

Because smart pointers use RAII. Their destructor releases the owned resource.

Common trap

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:

C++
Resource* r = new Resource();

doSomething();

delete r;

If doSomething() throws an exception, delete r might never execute.

With RAII:

C++
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
Common trap

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

C++
#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
Important property

unique_ptr cannot be copied.

C++
std::unique_ptr<int> p1 = std::make_unique<int>(10);

// std::unique_ptr<int> p2 = p1; // ERROR

But it can be moved.

C++
std::unique_ptr<int> p2 = std::move(p1);

Now:

p1 → nullptr
p2 → object
Common trap

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

C++
#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

Common trap

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

C++
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
Interview answer

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.

Common trap

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:

C++
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:

C++
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:

C++
unique_ptr(const unique_ptr&) = delete;

Conceptually.

How do you transfer ownership of unique_ptr?

Ownership is transferred using move semantics with std::move().

C++
auto p1 = std::make_unique<int>(10);

auto p2 = std::move(p1);

After this:

p1 → nullptr
p2 → 10
Important

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.

C++
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.

Common trap

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:

C++
auto p1 = std::make_shared<int>(10);

auto p2 = p1;
auto p3 = p1;

Conceptually:

p1 ──┐
p2 ──┼──► object
p3 ──┘

strong count = 3

Then:

C++
p2.reset();

Count becomes:

2

Then:

C++
p3.reset();

Count:

1

Finally:

C++
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.

Important

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.

C++
auto p1 = std::make_shared<int>(10);

std::cout << p1.use_count(); // 1

auto p2 = p1;

std::cout << p1.use_count(); // 2
Important trap

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:

C++
auto p = std::make_shared<int>(10);

std::weak_ptr<int> w = p;

The weak pointer doesn't keep 10 alive.

When:

C++
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:

C++
struct B;

struct A {
    std::shared_ptr<B> b;
};

struct B {
    std::shared_ptr<A> a;
};

Now:

C++
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:

C++
struct B {
    std::shared_ptr<A> a;
};

use:

C++
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

C++
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:

C++
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:

C++
auto p = std::make_unique<int>(10);

Instead of:

C++
std::unique_ptr<int> p(new int(10));

Advantages

  • Cleaner syntax
  • Clearly expresses ownership
  • Safer construction pattern
  • Avoids directly handling raw owning pointer
Interview answer

For normal ownership, I prefer make_unique over explicitly using new.

C++
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:

C++
auto p = std::make_shared<int>(10);

Instead of:

C++
std::shared_ptr<int> p(new int(10));

Conceptually:

make_shared

one allocation
┌─────────────────────────┐
│ control block + object  │
└─────────────────────────┘
C++
shared_ptr(new T)

Often involves separate allocations:

control block ───► object
Important nuance

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:

C++
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:

C++
void print(const int* p) {
    if (p) {
        std::cout << *p;
    }
}

The function doesn't own the object.

Another example

C++
auto p = std::make_unique<int>(10);

int* observer = p.get();

Here:

unique_ptr → owns
raw pointer → observes

The raw pointer must not call:

C++
delete observer;
Interview rule

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

C++
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.

Interview answer

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:

C++
void process(const Resource& resource);

or:

C++
void process(Resource* resource);

if null is meaningful.

Case 2 — Function takes ownership

Use unique_ptr by value:

C++
void process(std::unique_ptr<Resource> resource) {
    // owns resource
}

Call:

C++
auto p = std::make_unique<Resource>();

process(std::move(p));

Case 3 — Function shares ownership

Use shared_ptr by value:

C++
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:

C++
void process(const Resource& resource);

or:

C++
void process(const Resource* resource);

rather than unnecessarily passing shared_ptr.

Interview rule

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

C++
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

C++
std::shared_ptr<Resource> createResource() {
    return std::make_shared<Resource>();
}

Use this when shared ownership is actually required.

Important

Don't return a raw pointer to a local object:

C++
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

C++
#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.

Interview answer

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:

C++
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:

C++
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
Interviewer may ask

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:

C++
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);
        }
    }
C++
    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this != &other) {
            release();

            ptr = other.ptr;
            count = other.count;

            if (count) {
                ++(*count);
            }
        }
C++
        return *this;
    }

    ~MySharedPtr() {
        release();
    }

    size_t use_count() const {
        return count ? *count : 0;
    }

    T* get() const {
        return ptr;
    }
};

Usage:

C++
MySharedPtr<int> p1(new int(10));

MySharedPtr<int> p2 = p1;

std::cout << p1.use_count(); // 2
Important interview disclaimer

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:

C++
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);
        }
    }
C++
    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this != &other) {
            release();

            ptr = other.ptr;
            count = other.count;

            if (count) {
                ++(*count);
            }
        }
C++
        return *this;
    }

    ~MySharedPtr() {
        release();
    }

    size_t use_count() const {
        return count ? *count : 0;
    }

    T* get() const {
        return ptr;
    }
};

Usage:

C++
MySharedPtr<int> p1(new int(10));

MySharedPtr<int> p2 = p1;

std::cout << p1.use_count(); // 2
Important interview disclaimer

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:

C++
void process() {
    Resource* resource = new Resource();

    doSomething(resource);

    delete resource;
}

Problem

If:

C++
doSomething(resource);

throws an exception:

new
 ↓
doSomething()
 ↓
exception
 ↓
delete never executes
 ↓
memory leak

Convert to RAII

C++
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:

C++
#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?

Short interview answer

The major components are:

Containers

Algorithms

Iterators

Function objects / functors

Allocators

Example

C++
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.

C++
std::vector<int> v = {10, 20, 30};

v.push_back(40);

std::cout << v[2];

Conceptually:

10 | 20 | 30 | 40
 ↑    ↑    ↑    ↑
contiguous memory
Important properties
  • Dynamic size
  • Contiguous storage
  • O(1) random access
  • O(1) amortized push_back
  • Excellent cache locality
Interview trap

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:

C++
O(1)

after obtaining the iterator.

Disadvantages

  • No random access
  • Extra memory for pointers
  • Poor cache locality
  • Traversal is slower in practice than vector
C++
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.

C++
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:

C++
d[3];

What is array?

std::array is a fixed-size container that stores elements contiguously and has its size known at compile time.

C++
std::array<int, 3> a = {10, 20, 30};

Unlike a C-style array, it provides STL functionality:

C++
a.size();
a.begin();
a.end();
Important

Its size cannot change:

C++
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:

10 → 20 → 30 → 40

Compared with list:

forward_list → one link
list         → two links

It does not support:

C++
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.

C++
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

C++
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.

C++
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.

C++
std::priority_queue<int> pq;

pq.push(10);
pq.push(50);
pq.push(20);

std::cout << pq.top(); // 50

Min-heap

std::priority_queue<

C++
    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.

C++
std::set<int> s = {30, 10, 20, 10};
Result:
Output
10 20 30
Duplicate 10 is removed.

Typical implementation:

balanced search tree

Usually a red-black tree.

Operations are generally:

C++
O(log n)

What is multiset?

multiset is like set, but it allows duplicate values.

C++
std::multiset<int> ms = {
    10, 20, 10, 30
};
Result:
Output
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.

C++
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:

C++
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.

C++
std::multimap<int, std::string> m;

m.insert({1, "Alice"});
m.insert({1, "Bob"});

Both entries exist.

1 → Alice
1 → Bob
Important

Unlike map, this doesn't work the same way:

C++
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.

C++
std::unordered_set<int> s;

s.insert(10);
s.insert(20);
s.insert(10);

Contains:

10

20

Average operations:

C++
O(1)

Worst case:

C++
O(n)

What is unordered_map?

unordered_map stores key-value pairs using hashing, with unique keys and no sorted-order guarantee.

C++
std::unordered_map<std::string, int> freq;

freq["apple"]++;
freq["banana"]++;
freq["apple"]++;
Result:
Output
apple  → 2
banana → 1

Average:

C++
O(1)

Worst case:

C++
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
Interview answer

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:

C++
std::array<int, 5> a;
std::vector<int> v;
Interview answer

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

C++
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

C++
O(log n)

unordered_set

not sorted

C++
average O(1)

map vs multimap

map

Unique keys:

C++
std::map<int, std::string> m;

m[1] = "Alice";
m[1] = "Bob";

The second assignment changes the value.

multimap

Duplicate keys:

C++
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:

C++
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:

C++
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:

C++
std::vector<int> v;

v.reserve(100);

Then:

size = 0

capacity = 100

After:

C++
v.push_back(10);

size = 1

capacity = 100

C++
size() vs capacity()
std::vector<int> v;

v.reserve(10);

std::cout << v.size();     // 0
std::cout << v.capacity(); // at least 10
Remember

size = how many elements exist

capacity = how many elements can fit without reallocation

C++
reserve() vs resize()

This is a very common interview question.

C++
reserve()

Changes capacity:

C++
std::vector<int> v;

v.reserve(100);

size = 0

capacity ≥ 100

It does not create 100 elements.

C++
resize()

Changes size:

C++
v.resize(100);

Now:

size = 100

The elements are value-initialized as appropriate.

Interview answer

reserve() allocates storage without changing the number of elements, while resize() changes the number of elements.

How does vector grow?

When capacity is insufficient:

C++
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:

C++
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.

Result

Pointers, references, and iterators to elements of the old storage are invalidated.

This is extremely important.

Why is push_back() amortized O(1)?

Short interview answer

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:

1 → 2 → 4 → 8 → 16 ...

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

Important

Amortized O(1) does not mean every operation is O(1).

C++
push_back() vs emplace_back()

push_back

Adds an existing object/value:

C++
std::vector<std::pair<int, std::string>> v;

v.push_back({1, "Alice"});

emplace_back

Constructs the element directly in the vector:

C++
v.emplace_back(1, "Alice");
Interview answer

emplace_back constructs the element directly in the container using forwarded arguments, while push_back inserts an existing object or temporary.

Common trap

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.

Interview answer

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:

C++
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:

C++
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:

C++
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.

C++
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:

C++
std::ostream_iterator<int> out(std::cout, " ");

*out = 10;
++out;
C++
*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;
C++
--it;

Example:

list

set

map

Random-access iterator

Short interview answer

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?

Short interview answer

const_iterator allows us to traverse a container without modifying its elements through the iterator.

C++
std::vector<int> v = {10, 20, 30};

for (auto it = v.cbegin(); it != v.cend(); ++it) {
    std::cout << *it;
}

This is invalid:

C++
//*it = 100; // ERROR
Important distinction

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.

C++
std::vector<int> v = {10, 20, 30};

for (auto it = v.rbegin(); it != v.rend(); ++it) {
    std::cout << *it << " ";
}
Output:
Output
30 20 10
Important

With a reverse iterator:

++it

moves toward the beginning of the container.

C++
begin() vs cbegin()
begin()

Returns:

iterator

for a non-const container.

It can modify elements:

C++
auto it = v.begin();

*it = 100;
cbegin()

Returns:

const_iterator

C++
auto it = v.cbegin();

// *it = 100; // ERROR
Interview answer

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:

C++
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.

C++
std::vector<int> v = {10, 20, 30};

for (int x : v) {
    std::cout << x;
}

Conceptually, it uses begin() and end().

Modify elements

C++
for (int& x : v) {
    x *= 2;
}

Read-only

C++
for (const int& x : v) {
    std::cout << x;
}

Avoid unnecessary copies

Instead of:

C++
for (auto x : v)

use:

C++
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.

C++
std::vector<int> v = {4, 1, 3, 2};

std::sort(v.begin(), v.end());
Result:
Output
1 2 3 4

Descending order

C++
std::sort(v.begin(), v.end(), std::greater<int>());
Important

std::sort() requires a random-access iterator, so it works with:

vector

deque

array

but not directly with:

list

forward_list

For list, use:

C++
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

C++
Average: O(n log n)
Worst:   O(n log n)
Important

The standard specifies the complexity requirement, not a particular implementation strategy.

So say:

"Typically implemented using introsort."

rather than:

"C++ always uses quicksort."

C++
sort() vs stable_sort()
sort()
std::sort(v.begin(), v.end());

Equal elements are not guaranteed to preserve their original relative order.

C++
stable_sort()
std::stable_sort(v.begin(), v.end());

Equivalent elements preserve their relative ordering.

Example:

C++
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.

Interview answer

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.

C++
std::vector<int> v = {10, 20, 30};

auto it = std::find(v.begin(), v.end(), 20);

if (it != v.end()) {
    std::cout << "Found";
}

Complexity

C++
O(n)

for a general unsorted range.

What is find_if()?

find_if() finds the first element that satisfies a given condition.

C++
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.

C++
std::vector<int> v = {1, 2, 2, 3, 2};

int c = std::count(v.begin(), v.end(), 2);

std::cout << c; // 3

Complexity:

C++
O(n)

What is count_if()?

count_if() counts elements that satisfy a condition.

C++
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:

C++
std::vector<int> v = {1, 3, 3, 5, 7};

auto it = std::lower_bound(v.begin(), v.end(), 3);

Points to:

3

For:

C++
std::lower_bound(v.begin(), v.end(), 4);

it points to:

5

Complexity

For random-access iterators:

C++
O(log n)
Critical requirement

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:

C++
std::vector<int> v = {1, 3, 3, 5, 7};

auto it = std::upper_bound(v.begin(), v.end(), 3);

Points to:

5

Easy memory trick
lower_bound → >= x
upper_bound → >  x

What is binary_search()?

std::binary_search() checks whether a value exists in a sorted range.

C++
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.

C++
auto it = std::min_element(v.begin(), v.end());

std::cout << *it;

Complexity:

C++
O(n)

What is max_element()?

max_element() returns an iterator to the largest element in a range.

C++
auto it = std::max_element(v.begin(), v.end());

std::cout << *it;

Complexity:

C++
O(n)
Common trap

It returns an iterator, not the value itself.

What is reverse()?

std::reverse() reverses the elements in a range.

C++
std::vector<int> v = {1, 2, 3, 4};

std::reverse(v.begin(), v.end());
Result:
Output
4 3 2 1

Complexity:

C++
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.

C++
std::vector<int> v = {1, 2, 3, 4};

int sum = std::accumulate(
    v.begin(),
    v.end(),
    0
);
Result:
Output
10

Custom operation

C++
int product = std::accumulate(
    v.begin(),
    v.end(),
    1,
    std::multiplies<int>()
);
Important type trap

The type of the initial value matters.

C++
std::accumulate(v.begin(), v.end(), 0);

accumulates as an int.

If you need a long long result:

C++
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.

C++
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.

C++
erase()

Actually removes elements from the container.

C++
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(

C++
    std::remove(v.begin(), v.end(), 2),
    v.end()
);

This removes all 2s.

Why two operations?

Because:

remove → rearranges range
erase  → changes container size
Important modern C++

Since C++20, you can often write:

C++
std::erase(v, 2);

which is much cleaner for supported containers.

For a predicate:

C++
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(

C++
    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:

C++
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:

C++
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> result;

result.resize(v.size());

std::transform(

C++
    v.begin(),
    v.end(),
    result.begin(),
    [](int x) {
        return x * x;
    }
);
Result:
Output
1 4 9 16

Common interview use

Convert:

input → transformation → output

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:

C++
auto add = [](int a, int b) {
    return a + b;
};

std::cout << add(2, 3);
Result:
Output
5

Why useful?

Especially useful with STL algorithms:

C++
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;
});

Lambda syntax

General syntax:

C++
[capture](parameters) -> return_type {
    // body
}

Example:

C++
[](int a, int b) -> int {
    return a + b;
}

The return type can often be omitted:

C++
[](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:

C++
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:

C++
int x = 10;

auto f = [x]() {
    std::cout << x;
};

The lambda stores its own copy of x.

Changing the original:

C++
x = 20;

does not change the captured copy.

Common trap

By default, a value-captured variable cannot be modified inside the lambda:

C++
int x = 10;

auto f = [x]() {
    // x++; // ERROR
};

Unless the lambda is mutable.

Capture by reference

Syntax:

[&x]

Example:

C++
int x = 10;

auto f = [&x]() {
    x++;
};

f();

std::cout << x; // 11

The lambda accesses the original variable.

Important danger

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:

C++
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:

C++
int a = 10;
int b = 20;

auto f = [=]() {
    return a + b;
};

The lambda captures the needed local variables by value.

Important

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.

C++
int a = 10;
int b = 20;

auto f = [&]() {
    a++;
    b++;
};

Both refer to the original variables.

Main danger

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.

C++
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.

Important modern C++

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:

C++
int x = 10;

auto f = [x]() {
    // x++; // ERROR
};

With:

C++
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.

C++
auto add = [](auto a, auto b) {
    return a + b;
};

Now:

C++
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(

C++
    v.begin(),
    v.end(),
    [](int a, int b) {
        return a > b;
    }
);

Sort pairs by second value

std::sort(

C++
    v.begin(),
    v.end(),
    [](const auto& a, const auto& b) {
        return a.second < b.second;
    }
);

Sort students by marks

std::sort(

C++
    students.begin(),
    students.end(),
    [](const Student& a, const Student& b) {
        return a.marks > b.marks;
    }
);
Critical comparator rule

The comparator should define a strict weak ordering.

Don't write:

C++
return a <= b;

for a standard sorting comparator.

Use:

C++
return a < b;

Lambda vs function pointer

Lambda

C++
auto f = [](int x) {
    return x * 2;
};

A lambda has its own unique closure type.

It can also capture variables:

C++
int k = 10;

auto f = [k](int x) {
    return x * k;
};

Function pointer

C++
int (*f)(int);

A normal function pointer points to a function and cannot directly store captured state.

Important distinction

A non-capturing lambda can convert to a compatible function pointer:

C++
auto lambda = [](int x) {
    return x * 2;
};

int (*ptr)(int) = lambda;

A capturing lambda cannot implicitly convert to a regular function pointer:

C++
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().

C++
class Multiply {
public:
    int operator()(int a, int b) const {
        return a * b;
    }
};

Usage:

C++
Multiply multiply;

std::cout << multiply(2, 3);

Lambda:

C++
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:

C++
template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;
}

Now:

C++
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:

C++
int max(int a, int b);
double max(double a, double b);

With template:

C++
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.

C++
template <typename T>
T add(T a, T b) {
    return a + b;
}

Usage:

C++
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.

C++
template <typename T>
class Box {
private:
    T value;

public:
    Box(T v) : value(v) {}

    T get() const {
        return value;
    }
};

Usage:

C++
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.

C++
template <typename T>
class Box {
private:
    T value;

public:
    Box(T v) : value(v) {}

    T get() const {
        return value;
    }
};

Usage:

C++
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:

C++
template <typename T>
class Printer {
public:
    void print() {
        std::cout << "Generic";
    }
};

Specialization:

C++
template <>
class Printer<bool> {
public:
    void print() {
        std::cout << "Boolean";
    }
};

Now:

C++
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:

C++
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:

C++
template <typename T>
class Test {
};

Partial specialization for pointers:

C++
template <typename T>
class Test<T*> {
public:
    void print() {
        std::cout << "Pointer";
    }
};

Now:

C++
Test<int> a;    // primary template
Test<int*> b;   // partial specialization
Important

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?

Short interview answer

Variadic templates allow a template to accept a variable number of template arguments.

Example:

C++
template <typename... Args>
void print(Args... args) {
    (std::cout << ... << args);
}

Usage:

C++
print(10, 20, 30);
print("Hello", 10, 2.5);

The ... represents a parameter pack.

Common use cases

  • std::tuple
  • std::make_shared
  • std::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:

C++
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:

C++
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:

C++
template <typename T>
void print(T x) {
    cout << x;
}

It looks like any type can be passed.

For example:

C++
print(10);
print("Hello");
print(3.14);

These work because those types can be printed.

But suppose:

C++
class Student {
};

and:

C++
Student s;
print(s);

There is no:

C++
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:

C++
template <typename T>
void process(T obj) {
    obj.run();
}

If:

C++
class A {
public:
    void run() {}
};

class B {
public:
    void run() {}
};

Then:

C++
process(A{});
process(B{});

can generate different code at compile time.

Compare

Runtime polymorphism:

C++
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

C++
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

C++
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_cast
  • dynamic_cast
  • const_cast
  • reinterpret_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:

C++
double d = 10.5;

int x = static_cast<int>(d);
What interviewer may ask next

Which cast should you prefer?

Use the most specific C++ cast that expresses your intent. Avoid C-style casts when possible.

Common trap

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:

C++
double d = 10.5;
int x = static_cast<int>(d);

It can also be used for certain inheritance conversions:

C++
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

C++
int x = 10;
double y = static_cast<double>(x);

std::cout << y;
Common trap

static_cast does not perform runtime checking for a downcast.

C++
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:

C++
class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void show() {
        std::cout << "Derived\n";
    }
};

Now:

C++
Base* base = new Derived;

Derived* derived = dynamic_cast<Derived*>(base);

if (derived) {
    derived->show();
}

The runtime checks whether the object is actually a Derived.

Important requirement

For downcasting through a base pointer/reference, the base class generally needs to be polymorphic, meaning it has at least one virtual function.

Common trap

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

C++
const int x = 10;

int* p = const_cast<int*>(&x);

But this does not make it safe to modify x.

C++
*p = 20;  // Undefined behavior

because x was originally declared const.

Safe example

C++
int x = 10;

const int* p = &x;

int* q = const_cast<int*>(p);

*q = 20;  // OK

The original object was not actually const.

Common trap

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

C++
const int x = 10;

int* p = const_cast<int*>(&x);

But this does not make it safe to modify x.

C++
*p = 20;  // Undefined behavior

because x was originally declared const.

Safe example

C++
int x = 10;

const int* p = &x;

int* q = const_cast<int*>(p);

*q = 20;  // OK

The original object was not actually const.

Common trap

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

C++
int x = 10;

int* p = &x;

std::uintptr_t address =

C++
    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:

C++
double d = 10.5;

int* p = reinterpret_cast<int*>(&d);

Dereferencing p as an int can violate object/lifetime/alignment/aliasing rules.

Common trap

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:

C++
int x = (int)3.14;

C++:

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
Common trap

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

C++
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();
    }
}
Interview point

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

C++
Base* base = new Base;

Derived* derived = dynamic_cast<Derived*>(base);

if (derived == nullptr) {
    std::cout << "Cast failed\n";
}

Reference

C++
try {
    Derived& derived =
        dynamic_cast<Derived&>(*base);
}
catch (const std::bad_cast&) {
    std::cout << "Cast failed\n";
}
Remember
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:

C++
class Base {
public:
    virtual Base(); // Error
};

Can destructors be virtual?

Short interview answer

Yes.

In a polymorphic base class, the destructor should usually be virtual if objects may be deleted through a base pointer.

Example

C++
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
Common trap

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.

C++
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.

C++
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.

Important
C++
virtual ~Base() = 0;

makes the class abstract, but:

C++
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.

C++
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:

C++
void show(int x) override;

when the base has:

C++
virtual void show();

What does final do?

final prevents further overriding of a virtual function or further inheritance from a class.

Function

C++
class Base {
public:
    virtual void show() final {
    }
};

Now:

C++
class Derived : public Base {
    void show() override;  // Error
};

Class

C++
class Base final {
};

Now:

C++
class Derived : public Base { }; // Error
Interview tip

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

C++
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:

C++
Base& b = d;

or:

C++
Base* b = &d;

Base pointer vs derived pointer

Suppose:

C++
class Base {};
class Derived : public Base {};

Base pointer

C++
Base* b = new Derived;

This is an upcast.

The pointer can access only the interface available through Base*.

Derived pointer

C++
Derived* d = new Derived;

It can access both:

Base members
+
Derived members
Important

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:

C++
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:

C++
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

C++
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

C++
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();
}
Output:
Output
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

C++
Base* base = new Derived;

Derived* derived =

C++
    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.

C++
Base* base = new Derived;

Derived* derived =

C++
    dynamic_cast<Derived*>(base);

if (derived) {
    derived->show();
}

If base does not point to a Derived, the pointer becomes nullptr.

Compared with static_cast

C++
Derived* d = static_cast<Derived*>(base);

static_cast does not perform the same runtime check.

Example: Downcasting fails

C++
#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;
}
Output
Output
Downcasting failed

Why does it fail?

Look at this line:

C++
Base* b2 = new Base;

The actual object is:

b2
 ↓
┌──────────────┐
│ Base object  │
└──────────────┘

It is not a Derived object.

Then we ask:

C++
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

C++
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:

C++
Base* b1 = new Derived;
Base* b2 = new Base;

But the actual object type is different:

b1 → Derived object → downcast succeeds
b2 → Base object → downcast fails

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?

Short interview answer

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/static variables
  • code
  • open resources

Each thread typically has its own:

  • stack
  • registers
  • program counter
Common trap

Don't say threads have completely separate memory. Threads within the same process share the process's address space.


2. Process vs Thread

Process A Code / Data / Heap (own) Stack + registers (main thread) Process B (two threads) Code / Data / Heap (shared) Thread 1 stack Thread 2 stack Thread 1 registers Thread 2 registers
Figure: processes have separate memory; threads of one process share code, data and heap but each has its own stack.
Short interview answer

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
Interview point

Threads make communication easier but introduce synchronization problems.


3. How do you create a thread in C++?

Short interview answer

Use std::thread from the <thread> header.

C++
#include <thread>
#include <iostream>

void work() {
    std::cout << "Working\n";
}

int main() {
    std::thread t(work);

    t.join();
}
Important

The main thread and t execute concurrently when the scheduler allows it.


4. What is std::thread?

Short interview answer

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:

C++
std::thread t([] {
    std::cout << "Hello\n";
});

t.join();
Common trap

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()?

Short interview answer

join() makes the calling thread wait until the target thread finishes execution.

C++
std::thread t(work);

t.join();

std::cout << "Thread finished\n";

The output after join() executes is guaranteed to occur after work() finishes.

Important

A thread can be joined only once.


6. What is detach()?

Short interview answer

detach() separates the thread from the std::thread object, allowing the thread to continue independently.

C++
std::thread t(work);

t.detach();

After detaching, the std::thread object no longer represents the running thread.

Danger

You lose direct control over its lifetime.

For example, this is dangerous:

C++
void start() {
    int x = 10;

    std::thread t([&] {
        std::cout << x;
    });

    t.detach();
}

x may be destroyed while the detached thread still accesses it.

Interview point

Prefer join() when you need clear lifetime management.


7. What is joinable()?

Short interview answer

joinable() tells whether a std::thread object currently represents a thread of execution.

C++
std::thread t(work);

if (t.joinable()) {
    t.join();
}

A default-constructed thread is not joinable:

C++
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?

Short interview answer

If a std::thread object is destroyed while it is still joinable, the program calls std::terminate().

Example:

C++
{
    std::thread t(work);
} // std::terminate()

Correct approach

C++
{
    std::thread t(work);

    t.join();
}

or:

C++
{
    std::thread t(work);

    t.detach();
}
Important interview point

This behavior exists to prevent silently abandoning a running thread.


9. Passing arguments to a thread

Example

C++
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

C++
int x = 10;

std::thread t([x] {
    std::cout << x;
});

t.join();
Important

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:

C++
void increment(int& x) {
    ++x;
}

This won't work as a normal copied argument when you intend reference semantics.

Use:

C++
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.

Common trap

The referenced object must remain alive while the thread accesses it.


Race Conditions

11. What is a race condition?

Short interview answer

A race condition occurs when the program's result depends on the timing or ordering of concurrent operations.

Example:

C++
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?

Short interview answer

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:

C++
int counter = 0;

void work() {
    ++counter;
}

If two threads execute work() concurrently without synchronization, there is a data race.

Very important
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?

Short interview answer

Use proper synchronization or avoid shared mutable state.

Common techniques:

  • std::mutex
  • std::atomic
  • immutable data
  • thread-local data
  • message passing

Mutex example

C++
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?

Short interview answer

Thread safety means code behaves correctly when accessed concurrently by multiple threads.

Example:

C++
class Counter {
    std::mutex m;
    int value = 0;

public:
    void increment() {
        std::lock_guard<std::mutex> lock(m);
        ++value;
    }
C++
};

Multiple threads can safely call increment().

Important

Thread-safe doesn't necessarily mean lock-free or highly performant.


16. What is shared state?

Short interview answer

Shared state is data that can be accessed by multiple threads.

Examples:

C++
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?

Short interview answer

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?

Short interview answer

std::mutex is the standard C++ mutual exclusion primitive used to protect shared data.

C++
std::mutex m;
int counter = 0;

void increment() {
    std::lock_guard<std::mutex> lock(m);
    ++counter;
}

19. What does lock() do?

Short interview answer

lock() acquires the mutex. If another thread already owns it, the calling thread waits until it becomes available.

C++
m.lock();

counter++;

m.unlock();

Problem

Manual locking is easy to get wrong.


20. What does unlock() do?

Short interview answer

unlock() releases the mutex so another waiting thread can acquire it.

C++
m.lock();

counter++;

m.unlock();
Danger

If an exception occurs before unlock():

C++
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?

Short interview answer

std::lock_guard is a simple RAII wrapper that locks a mutex when constructed and unlocks it when destroyed.

C++
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?

Short interview answer

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:

C++
std::unique_lock<std::mutex> lock(m);

counter++;

lock.unlock();

// do other work

lock.lock();

It can also defer locking:

C++
std::unique_lock<std::mutex> lock(m, std::defer_lock);

lock.lock();
Key idea

More flexible than lock_guard, but has slightly more overhead/complexity.


23. What is std::scoped_lock?

Short interview answer

std::scoped_lock is an RAII locking wrapper that can lock one or multiple mutexes safely.

Example:

C++
std::scoped_lock lock(m1, m2);

It is especially useful when multiple mutexes must be acquired together.

Important

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
Interview answer

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
C++
std::unique_lock<std::mutex> lock(m);

scoped_lock

Best when you want to acquire multiple mutexes together:

C++
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?

Short interview answer

RAII ensures the mutex is automatically released when the lock object leaves scope, even if an exception occurs.

C++
{
    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:

C++
m.lock();

doSomething();

m.unlock();

If doSomething() throws:

lock()
  ↓
exception
  ↓
unlock() never runs
  ↓
mutex remains locked

Other threads can block indefinitely.

Better:

C++
std::lock_guard<std::mutex> lock(m);
doSomething();

Deadlocks

28. What is a deadlock?

Short interview answer

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
Interview trick

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_lock with appropriate retry/backoff strategies

Most common answer

Establish a consistent global ordering for acquiring locks.


31. What is lock ordering?

Suppose there are:

C++
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?

Short interview answer

std::lock locks multiple mutexes using a deadlock-avoidance algorithm.

Example:

C++
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:

C++
std::scoped_lock lock(m1, m2);

33. What is std::scoped_lock for deadlock prevention?

For multiple mutexes:

C++
std::scoped_lock lock(m1, m2);

It acquires them using a deadlock-avoidance strategy.

This is preferable to manually doing:

C++
m1.lock();
m2.lock();

because different threads could acquire them in opposite orders.


34. What is try_lock?

Short interview answer

try_lock() attempts to acquire a mutex without blocking indefinitely.

C++
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

...

Easy memory trick
Deadlock   → stuck waiting
Starvation → never gets chance
Livelock   → moving but not progressing

Atomic

36. What is std::atomic?

Short interview answer

std::atomic provides atomic operations on shared data, allowing certain operations to occur without a data race.

Example:

C++
std::atomic<int> counter{0};

void increment() {
    ++counter;
}

Multiple threads can safely increment it.

Important

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:

C++
std::atomic<int> counter;
counter++;

Mutex:

C++
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?

Short interview answer

Use std::atomic when shared state can be correctly represented through atomic operations, such as counters, flags, or simple state variables.

Example:

C++
std::atomic<bool> stop{false};

Worker:

C++
while (!stop.load()) {
    // work
}

Another thread:

C++
stop.store(true);
Common trap

Don't replace every mutex with an atomic.


39. What is load()?

Short interview answer

load() atomically reads the value of an atomic object.

C++
std::atomic<int> x{10};

int value = x.load();

You can specify memory ordering:

C++
x.load(std::memory_order_relaxed);

40. What is store()?

Short interview answer

store() atomically writes a value to an atomic object.

C++
std::atomic<int> x{10};

x.store(20);

Memory ordering can also be specified.


41. What is fetch_add()?

Short interview answer

fetch_add() atomically adds a value and returns the previous value.

C++
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?

Short interview answer

compare_exchange atomically compares an atomic value with an expected value and replaces it with a desired value if they match.

Example:

C++
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?

Short interview answer

CAS means Compare-And-Swap or Compare-And-Exchange.

Conceptually:

C++
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?

Short interview answer

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

C++
while (true) {
    int old = value.load();

    if (value.compare_exchange_weak(old, old + 1))
        break;
}
Important distinction

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?

Short interview answer

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?

Short interview answer

memory_order_relaxed guarantees atomicity of the atomic operation but provides no synchronization ordering with other memory accesses.

Example:

counter.fetch_add(

C++
    1,
    std::memory_order_relaxed
);

Good for independent counters where you don't need to publish other data.

Key idea

Atomic? Yes

Synchronization? No


47. What is memory_order_acquire?

Short interview answer

An acquire operation prevents subsequent memory operations from being reordered before it and can synchronize with a corresponding release operation.

Typical pattern:

C++
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?

Short interview answer

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:

C++
data = 42;

ready.store(

C++
    true,
    std::memory_order_release
);

Another thread:

C++
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.

Interview answer

Release is generally used when publishing data; acquire is used when consuming that published data.


Synchronization

50. What is condition_variable?

Short interview answer

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::mutex
  • std::unique_lock

Example:

C++
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:

C++
{
    std::lock_guard<std::mutex> lock(m);
    ready = true;
}

cv.notify_one();

51. What is the producer-consumer problem?

Short interview answer

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:

C++
std::queue<int> q;
std::mutex m;
std::condition_variable cv;

Producer:

C++
{
    std::lock_guard<std::mutex> lock(m);
    q.push(10);
}

cv.notify_one();

Consumer:

C++
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()

C++
notify_one()

Wakes one waiting thread.

C++
cv.notify_one();

Useful when one newly available resource can be consumed by one worker.

C++
notify_all()

Wakes all waiting threads.

C++
cv.notify_all();

All awakened threads re-check the condition.

Interview point

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:

C++
cv.wait(lock);

prefer:

C++
cv.wait(lock, [] {
    return !q.empty();
});

Why?

Because waking up does not necessarily mean the desired condition is true.

The predicate effectively behaves like:

C++
while (!condition) {
    wait();
}

This handles spurious wakeups and state changes correctly.


54. What are spurious wakeups?

Short interview answer

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:

C++
cv.wait(lock);

does not mean:

"The condition is definitely true."

Instead, always check the condition again.

Preferred:

C++
cv.wait(lock, [] {
    return ready;
});

Conceptually:

C++
while (!ready) {
    cv.wait(lock);
}
Common trap

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 / decltype
  • constexpr / noexcept
  • exception safety
  • compilation vs linking
  • ODR
  • perfect forwarding
  • forwarding references
  • reference collapsing
  • SFINAE vs concepts
  • CRTP
  • volatile vs atomic

I'll keep the same interview format: short answer → simple explanation → code → follow-up → trap.

Modern C++

1. What is auto?

Short interview answer

auto lets the compiler deduce the type of a variable from its initializer at compile time.

C++
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.

Common trap
C++
auto x;

is invalid because there is no initializer from which to deduce the type.


2. What is decltype?

Short interview answer

decltype obtains the type of an expression without evaluating that expression.

C++
int x = 10;

decltype(x) y = 20;

Here:

decltype(x) → int
Important difference

decltype follows special rules for value categories.

C++
int x = 10;

decltype(x)  a = x;  // int
decltype((x)) b = x; // int&

Why?

x → unparenthesized variable → declared type int
(x) → lvalue expression → int&

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:

C++
int x = 10;

auto a = x;
decltype(x) b = x;
decltype((x)) c = x;

Types:

a → int
b → int
c → int&
Interview answer

auto deduces a variable's type from its initializer, while decltype determines the type associated with an expression.


4. What is constexpr?

Short interview answer

constexpr indicates that a variable or function can participate in compile-time evaluation when given constant-expression inputs.

Example:

C++
constexpr int square(int x) {
    return x * x;
}

constexpr int x = square(5);

The compiler can evaluate:

25

at compile time.

Important

constexpr does not necessarily mean:

"This always executes at compile time."

A constexpr function can also be called at runtime with non-constant inputs.

C++
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.

C++
const int x = 10;

But its value doesn't necessarily have to be known at compile time:

C++
int n;
std::cin >> n;

const int x = n;

constexpr

Requires the initialized value to be usable as a constant expression.

C++
constexpr int x = 10;
Easy distinction

const

→ cannot modify

constexpr

→ compile-time evaluable constant

A constexpr variable is also const.


6. What is noexcept?

Short interview answer

noexcept specifies that a function is not expected to throw exceptions.

C++
void process() noexcept {
}

You can also query whether an expression is noexcept:

C++
bool b = noexcept(process());
Important

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?

Short interview answer

Because standard containers such as std::vector prefer moving elements during reallocation when the move constructor is known not to throw.

Example:

C++
class A {
public:
    A(A&& other) noexcept {
    }
};

When a vector grows:

C++
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.

Interview line

Marking a move constructor noexcept allows containers to safely prefer moving over copying when appropriate.


8. What is nullptr?

Short interview answer

nullptr is the C++11 null pointer literal with type std::nullptr_t.

C++
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:

C++
void f(int);
void f(int*);

f(nullptr); // calls f(int*)
Interview trap

nullptr is not an integer.


9. What is std::optional?

Short interview answer

std::optional<T> represents a value that may or may not exist.

Example:

C++
std::optional<int> findValue(bool found) {
    if (found)
        return 42;

    return std::nullopt;
}

Usage:

C++
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?

Short interview answer

std::variant is a type-safe union that can hold one value from a fixed set of types.

C++
std::variant<int, std::string> value;

value = 10;
value = "hello";

Only one alternative is active at a time.

Access

C++
std::get<int>(value);

or:

C++
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?

Short interview answer

std::string_view is a non-owning view of a sequence of characters.

C++
void print(std::string_view s) {
    std::cout << s;
}

You can pass:

C++
print("Hello");

or:

C++
std::string str = "Hello";
print(str);

Advantage

It can avoid unnecessary string copies.

Major danger

It does not own the characters.

C++
std::string_view getName() {
    std::string name = "Dilip";
    return name; // dangling view
}

The std::string is destroyed when the function returns.

Interview trap
string       → owns data
string_view  → observes data

12. What is std::function?

Short interview answer

std::function is a type-erased callable wrapper that can store different callable objects with a compatible signature.

C++
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::bind results
  • 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?

Short interview answer

A functor, or function object, is an object that can be called like a function by defining operator().

C++
class Add {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

Usage:

C++
Add add;

std::cout << add(2, 3);

Why useful?

Functors can maintain state.

C++
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:

C++
auto add = [](int a, int b) {
    return a + b;
};

Functor:

C++
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?

Short interview answer

An exception is a mechanism for reporting and handling abnormal conditions separately from normal program flow.

Example:

C++
throw std::runtime_error("File not found");

A caller can handle it:

C++
try {
    // code
}
catch (const std::exception& e) {
    std::cout << e.what();
}

16. try, catch, throw

C++
try

Contains code that may throw.

C++
try {
    process();
}
throw

Signals an exception.

C++
throw std::runtime_error("Error");

catch

Handles the exception.

C++
catch (const std::exception& e) {
    std::cout << e.what();
}

Flow

try
 ↓

exception?

 ↓ yes
throw
 ↓
matching catch

17. What is exception safety?

Short interview answer

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?

Short interview answer

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?

Short interview answer

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?

Short interview answer

The no-throw guarantee means an operation promises not to throw exceptions.

C++
void cleanup() noexcept {
}

The standard library provides many operations with noexcept guarantees.

Important

A noexcept function that allows an exception to escape results in std::terminate().


21. RAII and exceptions

Short interview answer

RAII ensures resources are released automatically when objects leave scope, including when stack unwinding happens due to an exception.

Example:

C++
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?

Short interview answer

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:

C++
~Resource() noexcept {
    cleanup();
}
Interview point

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:

C++
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()
Interview answer

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?

Short interview answer

The preprocessor performs source-level transformations before the actual compilation phase.

Examples:

C++
#include <iostream>
#define MAX 100

#ifdef DEBUG
// ...
#endif

It handles directives such as:

  • #include
  • #define
  • #if
  • #ifdef
Important

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:

C++
void foo();

int main() {
    foo();
}

Compilation can succeed, but linking can fail with an undefined reference/unresolved external symbol.

Easy memory trick
Compilation → source → object
Linking → objects → executable

26. What are header files?

Short interview answer

Header files typically contain declarations, type definitions, templates, inline definitions, and other interfaces shared between source files.

Example:

C++
// math.h
int add(int a, int b);

Implementation:

C++
// math.cpp
int add(int a, int b) {
    return a + b;
}

Usage:

C++
#include "math.h"
Important

Headers are generally included into translation units by the preprocessor.


27. What are include guards?

Short interview answer

Include guards prevent the contents of a header from being processed multiple times within the same translation unit.

C++
#ifndef MY_HEADER_H
#define MY_HEADER_H

class Test {
};

#endif

Without protection, repeated inclusion could cause redefinition errors.


28. What is #pragma once?

Short interview answer
C++
#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
C++
#pragma once
→ widely supported but technically non-standard directive
Interview answer

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.

C++
int add(int, int);

Definition

Provides the implementation/storage.

C++
int add(int a, int b) {
    return a + b;
}

For a variable:

C++
extern int x; // declaration
int x = 10;   // definition
Easy memory trick
Declaration → "this exists"
Definition  → "this is what it is"

30. What is the One Definition Rule?

Short interview answer

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 inline functions/classes can have identical definitions in multiple translation units under specific ODR rules

Example problem

C++
// 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:

C++
// header.h
extern int x;
// source.cpp
int x = 10;
Important

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?

Short interview answer

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
Important

If you have:

main.cpp

utils.cpp

you generally have two separate translation units.


Advanced Language

33. What is perfect forwarding?

Short interview answer

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:

C++
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?

Short interview answer

std::forward conditionally casts a forwarding-reference parameter back to the value category it had at the call site.

C++
template <typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

If caller passes:

C++
std::string s;

wrapper(s);

T is deduced as:

std::string&

If caller passes:

C++
wrapper(std::string("hello"));

T is deduced as:

std::string

std::forward<T>(arg) preserves the corresponding lvalue/rvalue category.

Critical distinction

std::move

→ unconditionally casts toward an rvalue/xvalue

std::forward

→ conditionally preserves the caller's value category

35. What is a universal/forwarding reference?

Short interview answer

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.

C++
template <typename T>
void foo(T&& value) {
}

It can accept both lvalues and rvalues.

Example:

C++
int x = 10;

foo(x);       // lvalue
foo(10);      // rvalue
Important terminology

"Universal reference" is older terminology popularized by Scott Meyers.

The standard terminology is forwarding reference.


36. What is template type deduction?

Consider:

C++
template <typename T>
void foo(T&& x);

For an lvalue:

C++
int a = 10;

foo(a);

T becomes:

int&

Then:

T&&

→ int& &&

which collapses to:

int&

For an rvalue:

C++
foo(10);

T becomes:

int

Therefore:

T&& → int&&

This is the basis of perfect forwarding.


37. What is reference collapsing?

Short interview answer

Reference collapsing defines what happens when references are combined.

The four important rules are:

T&  &   → T&
T&  &&  → T&
T&& &   → T&
T&& &&  → T&&
Easy rule

If there is an &, the result is generally an lvalue reference.

Only:

&& + && → &&

remains an rvalue reference.

Example

C++
template <typename T>
void foo(T&& x);

If:

C++
int a = 10;
foo(a);

then:

T = int&

T&& = int& &&

C++
    = int&

38. What is SFINAE?

Short interview answer

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:

C++
template <typename T>

typename std::enable_if<std::is_integral_v<T>>::type

C++
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.

Common trap

SFINAE doesn't mean all template errors are silently ignored. It applies in specific substitution contexts.


39. What are concepts?

Short interview answer

Concepts are a C++20 feature used to specify constraints on template parameters.

Example:

C++
template <typename T>

requires std::integral<T>

C++
T add(T a, T b) {
    return a + b;
}

Or:

C++
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
  • explicit constraints
  • easier-to-read generic code
Interview comparison

SFINAE

→ older template constraint technique

Concepts

→ modern C++20 constraint mechanism

40. What is CRTP?

Short interview answer

CRTP, or Curiously Recurring Template Pattern, is a technique where a class derives from a template instantiated with the derived class itself.

C++
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
  • static dispatch
  • code reuse
  • avoiding virtual-function overhead in suitable designs
Important

CRTP is different from traditional runtime polymorphism:

Virtual functions → runtime dispatch
CRTP              → compile-time/static dispatch

41. What is friend?

Short interview answer

friend allows a specified function or class to access private and protected members of another class.

Example:

C++
class Person {
private:
    int age = 20;

    friend void showAge(const Person&);
};

void showAge(const Person& p) {
    std::cout << p.age;
}
Important properties

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?

Short interview answer

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:

C++
class Cache {
    mutable int calls = 0;

public:
    int get() const {
        ++calls;
C++
        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?

Short interview answer

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:

C++
volatile int hardwareRegister;

It is relevant to certain:

  • memory-mapped hardware
  • signal-handler-related scenarios
  • low-level systems code
Very important interview trap

volatile does not provide thread synchronization and does not make operations atomic.

Wrong:

C++
volatile int counter;

and assuming multiple threads can safely increment it.

For thread synchronization, use:

std::atomic<int>

or an appropriate mutex.

Easy distinction

volatile

→ special memory-access semantics

atomic

→ thread-safe atomic operations / synchronization primitives

44. What is static_assert?

Short interview answer
C++
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

C++
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

C++
assert()
→ runtime check
C++
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:

Interview answer

Simple explanation

C++ code

What interviewer may ask next
Common trap

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.

C++
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.

C++
vector<int> nums = {5, 2, 8, 1};

sort(nums.begin(), nums.end(), [](int a, int b) {
    return a > b;
});
Result:
Output
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

C++
struct Student {
    string name;
    int marks;
};

vector<Student> students = {
    {"A", 80},
    {"B", 95},
    {"C", 70}
};

sort(students.begin(), students.end(),

C++
     [](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:

C++
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:

C++
priority_queue<int, vector<int>, greater<int>> pq;

pq.push(5);
pq.push(2);
pq.push(8);

cout << pq.top(); // 2

For objects:

C++
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.

C++
vector<int> nums = {4, 2, 4, 1, 2};

set<int> s(nums.begin(), nums.end());

for (int x : s)
    cout << x << " ";
Output:
Output
1 2 4

If ordering isn't needed:

C++
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.

C++
map<int, string> mp;

mp[3] = "C";
mp[1] = "A";
mp[2] = "B";

for (auto& [key, value] : mp) {
    cout << key << " " << value << '\n';
}
Output:
Output
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.

C++
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;
    }
C++
    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.

C++
vector<int> nums = {1, 3, 5, 7, 9};

bool found = binary_search(nums.begin(), nums.end(), 5);

For the position:

C++
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.

C++
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);
}
Result:
Output
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
C++
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

C++
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:

C++
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

C++
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

C++
struct Node {
    int data;
    Node* prev;
    Node* next;

    Node(int x)
        : data(x), prev(nullptr), next(nullptr) {}
};

Insertion at front:

C++
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:

C++
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:

C++
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.

Interviewer may ask:

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:

C++
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:

C++
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);
    }
C++
    ~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:

C++
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_map for O(1) lookup

unordered_map

key → list iterator

list

MRU → ... → LRU

Implementation:

C++
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:

C++
struct Employee {
    string name;
    int salary;
};

struct Compare {
    bool operator()(const Employee& a,
                    const Employee& b) const {
        return a.salary < b.salary;
    }
};

Usage:

C++
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:

C++
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:

C++
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:

C++
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:

C++
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:

C++
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

C++
class A {
public:
    A() {
        cout << "Constructor\n";
    }

    ~A() {
        cout << "Destructor\n";
    }
};

int main() {
    A obj;
}
Output:
Output
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

C++
class A {
public:
    A() {
        cout << "Default\n";
    }

    A(const A&) {
        cout << "Copy\n";
    }
};

int main() {
    A a;
    A b = a;
}
Output:
Output
Default
Copy

A b = a initializes a new object from an existing object, so the copy constructor is selected.

Trap:
C++
b = a;

after b already exists is copy assignment, not copy construction.


28. Move constructor output

C++
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);
}
Output:
Output
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

C++
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;
}
Output:
Output
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

C++
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();
}
Output:
Output
Base
Why?
Derived object
     ↓ copy by value
Base object
     ↓
Derived part sliced away

This is object slicing.


31. Static variable output

C++
class Counter {
public:
    static int count;

    Counter() {
        ++count;
    }
C++
};

int Counter::count = 0;

int main() {
    Counter a;
    Counter b;
    Counter c;

    cout << Counter::count;
}
Output:
Output
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++

C++
int i = 5;

cout << ++i;
Output:
Output
6

Pre-increment:

increment
   ↓
return new value

Post-increment:

C++
int i = 5;

cout << i++;
Output:
Output
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

C++
int arr[] = {10, 20, 30, 40};

int* p = arr;

cout << *(p + 2);
Output:
Output
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

C++
*(arr + 2)

refer to the same element.

Trap: Pointer arithmetic is scaled by the pointed-to type.


34. Reference behavior

C++
int x = 10;

int& ref = x;

ref = 20;

cout << x;
Output:
Output
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

C++
const int x = 10;

// x = 20;   // Error

A const object cannot be modified through that const access.

With a pointer:

C++
const int* p = &x;

means:

pointer to const int

You cannot modify *p through p.

But:

C++
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

C++
int arr[5];

cout << sizeof(arr);

If sizeof(int) == 4, output:

20

Because:

5 × 4 = 20

But:

C++
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

C++
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

C++
int x = 10;

auto f = [x]() mutable {
    x++;
    cout << x << '\n';
};

f();
cout << x;
Output:
Output
11
10

The lambda captured its own copy of x.

mutable allows modification of that captured copy.

Compare:

C++
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

C++
auto p1 = make_shared<int>(10);

cout << p1.use_count() << '\n';

{
    auto p2 = p1;
    cout << p1.use_count() << '\n';
}

cout << p1.use_count();
Output:
Output
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:

C++
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