Chapters 7 through 9. Matrix traversal, diagonal properties, and rotation. Intermediate challenges spanning binary search, DP, greedy, and classic algorithms. Advanced C++ OOP: memory management, move semantics, design patterns, and the key architectural idioms every serious programmer must own.
Find the sum of all elements in a 2D matrix.
[[1,2],[3,4]]10Row 0: 1 + 2 = 3
Row 1: 3 + 4 = 7
Total: 10int matrixSum(vector<vector<int>>& m){
int sum=0;
for(auto& row : m)
for(int x : row) sum += x;
return sum;
}Time: O(R×C) | Space: O(1). The range-for over a 2D vector accesses rows by reference — no index arithmetic needed.
For a square n×n matrix: (A) find the sum of both diagonals combined. (B) find the absolute difference between the primary and secondary diagonal sums.
Primary diagonal: element at (i, i) for all i.
Secondary diagonal: element at (i, n−1−i) for all i.
When n is odd, the centre element (n/2, n/2) lies on both diagonals — subtract it once to avoid double-counting in part (A).
Primary (i,i): 1 + 5 + 9 = 15 Secondary (i,n-1-i): 3 + 5 + 7 = 15 Centre element (5) counted twice → subtract once Both diagonals sum = 15+15-5 = 25 Difference = |15-15| = 0
int diagonalSum(vector<vector<int>>& m){
int n=m.size(), sum=0;
for(int i=0; i<n; i++){
sum += m[i][i]; // primary
sum += m[i][n-1-i]; // secondary
}
if(n%2) sum -= m[n/2][n/2]; // centre counted twice for odd n
return sum;
}
int diagDiff(vector<vector<int>>& m){
int n=m.size(), p=0, s=0;
for(int i=0; i<n; i++){ p += m[i][i]; s += m[i][n-1-i]; }
return abs(p-s);
}Time: O(n) | Space: O(1). Single loop handles both diagonals simultaneously — the centre-subtraction guard is the key correctness detail.
Check if two matrices are identical: same dimensions and every corresponding element is equal.
A[1][1]=4 != B[1][1]=5 → Not identicalbool identical(vector<vector<int>>& A,
vector<vector<int>>& B){
if(A.size()!=B.size() || A[0].size()!=B[0].size())
return false;
return A==B; // vector == does element-wise comparison
}Time: O(R×C) | Space: O(1). vector::operator== compares element-by-element recursively — it's correct and idiomatic. Always check dimensions first to short-circuit early.
A matrix is Toeplitz if every top-left-to-bottom-right diagonal has the same value. Check if a given matrix is Toeplitz.
All elements on the same diagonal share the property that row − col is constant. So the check reduces to: for every element (i, j) where i>0 and j>0, verify m[i][j] == m[i−1][j−1].
m[1][1]=1 == m[0][0]=1 ✓
m[1][2]=2 == m[0][1]=2 ✓
m[2][1]=4 == m[1][0]=4 ✓
m[2][2]=1 == m[1][1]=1 ✓
Toeplitz ✓bool isToeplitz(vector<vector<int>>& m){
int R=m.size(), C=m[0].size();
for(int i=1; i<R; i++)
for(int j=1; j<C; j++)
if(m[i][j] != m[i-1][j-1]) return false;
return true;
}Time: O(R×C) | Space: O(1). Each element is compared only once against its upper-left neighbour. This diagonal-equality property generalises: elements on anti-diagonals share a constant (row + col).
Find the number of diagonals in an n-sided polygon. A diagonal connects two non-adjacent vertices.
Total vertex pairs = C(n,2) = n(n−1)/2. Subtract the n sides (adjacent pairs). Result: n(n−3)/2.
Triangle (n=3): 3×0/2 = 0 ✓ Square (n=4): 4×1/2 = 2 ✓ Pentagon (n=5): 5×2/2 = 5 ✓ Hexagon (n=6): 6×3/2 = 9 ✓
long long numDiagonals(int n){
return (long long)n * (n-3) / 2;
}
// Guard: n must be >= 3 (a polygon has at least 3 vertices)Time: O(1) | Space: O(1). Pattern: "count pairs, subtract forbidden pairs" — this combinatorial template solves dozens of counting problems.
A number n is automorphic if its square ends with the digits of n. Examples: 5² = 25 ✓, 76² = 5776 ✓, 376² = 141376 ✓.
Count the digits d in n. Compute n². Check if n² mod 10^d equals n. Use integer power-of-ten rather than pow() to avoid floating-point issues.
d = 2 digits
n² = 5776
mod = 10² = 100
5776 % 100 = 76 == n → Automorphic ✓bool isAutomorphic(int n){
long long sq = (long long)n * n;
int digits = to_string(n).size();
long long mod = 1;
for(int i=0; i<digits; i++) mod *= 10; // 10^digits (avoid pow)
return sq % mod == n;
}Time: O(d) | Space: O(1). Never use pow(10, digits) for integer exponentiation — floating-point rounding can give 99 instead of 100, producing wrong modulo results.
Given a and b (b ≥ 2), check whether a = b^k for some integer k ≥ 0.
a=27, b=3Yes (3³)Keep dividing a by b. If a reaches 1 exactly, it was a power of b. If at any step a is not divisible by b, it's not. Edge: a=1 is b^0=1 for any b≥2 → true.
27 % 3 == 0 → a = 27/3 = 9 9 % 3 == 0 → a = 9/3 = 3 3 % 3 == 0 → a = 3/3 = 1 a == 1 → Yes, 3³ = 27 ✓ a=12, b=3 12 % 3 == 0 → a = 4 4 % 3 != 0 → Not a power of 3 ✗
bool isPowerOf(int a, int b){
if(b < 2 || a <= 0) return false;
while(a % b == 0) a /= b;
return a == 1; // a == 1 means fully divided away
}
// isPowerOf(1, any) = true (b^0 = 1)
// isPowerOf(27,3) = true (3^3)Time: O(log_b(a)) | Space: O(1). The loop runs at most log_b(a) times — same as the exponent. For b=2 and a=2^30, that's 30 iterations.
Given n, find the largest power of 2 ≤ n (floor) and the smallest power of 2 ≥ n (ceil). Example: n=13 → floor=8, ceil=16.
The floor power of 2 is obtained by computing 1 << floor(log2(n)). If n is already a power of 2, both floor and ceil equal n. Otherwise ceil = floor × 2.
log2(13) ≈ 3.70 → floor = 3 lo = 1 << 3 = 8 (8 ≤ 13) hi = 8 << 1 = 16 (16 ≥ 13) Result: lo=8, hi=16 n = 16 (exact power of 2) lo = 1 << 4 = 16 → hi = 16 (already ≥ n, no shift needed) Result: lo=16, hi=16
pair<int,int> nearestPow2(int n){
if(n <= 0) return {0, 1};
int lo = 1 << (int)log2(n); // largest power of 2 <= n
int hi = (lo == n) ? lo : lo << 1; // smallest power of 2 >= n
return {lo, hi};
}
// n=13 → {8, 16} | n=16 → {16, 16}Time: O(1) | Space: O(1). Left-shift by k is multiplication by 2^k. Checking lo == n before shifting avoids doubling an exact power-of-two unnecessarily.
Print a triangle where row i contains i consecutive odd numbers, continuing from where the previous row left off. The sum of each row equals i³.
1 → 1³ = 1 3 5 → 2³ = 8 7 9 11 → 3³ = 27 13 15 17 19 → 4³ = 64
Row 2: 3+5 = 8 = 2³ ✓ Row 3: 7+9+11 = 27 = 3³ ✓ Row 4: 13+15+17+19 = 64 = 4³ ✓
void oddTriangle(int n){
int num = 1;
for(int i=1; i<=n; i++){
for(int j=0; j<i; j++){
cout << num << " ";
num += 2; // advance to next odd number
}
cout << "\n";
}
}Time: O(n²) | Space: O(1). The row-sum = i³ property is a classic number theory result. This can be proven by noting row i starts at i²−i+1 and contains i terms of an AP with d=2.
Find ⌊√n⌋ — the floor integer square root of n — without using sqrt().
The answer lies in [1, n]. Binary search: if mid² ≤ n, it's a candidate — record it and search higher. If mid² > n, search lower. When lo > hi, ans holds the last valid mid.
lo=1, hi=35, ans=1
mid=18: 18²=324 > 35 → hi=17
mid= 9: 9²= 81 > 35 → hi= 8
mid= 4: 4²= 16 ≤ 35 → ans=4, lo=5
mid= 6: 6²= 36 > 35 → hi=5
mid= 5: 5²= 25 ≤ 35 → ans=5, lo=6
lo=6 > hi=5 → Answer: 5 (5²=25≤35, 6²=36>35)int floorSqrt(int n){
if(n < 2) return n;
long long lo=1, hi=n, ans=1;
while(lo <= hi){
long long mid = lo + (hi-lo)/2;
if(mid*mid == n) return (int)mid;
if(mid*mid < n){ ans=mid; lo=mid+1; }
else hi=mid-1;
}
return (int)ans;
}Time: O(log n) | Space: O(1). Use long long for mid to prevent overflow when mid² is computed — for n near INT_MAX, mid can be ~46,000 and mid² overflows int.
Print all Armstrong numbers between L and R. A number is Armstrong (narcissistic) if it equals the sum of each digit raised to the power of its digit count. Example: 153 = 1³+5³+3³.
digits = 3 1³ + 5³ + 3³ = 1 + 125 + 27 = 153 == n → Armstrong ✓ n = 370 3³ + 7³ + 0³ = 27 + 343 + 0 = 370 == n → Armstrong ✓
bool isArmstrong(int n){
string s = to_string(n);
int d = s.size(), sum = 0;
for(char c : s){
int digit = c - '0';
int pw = 1;
for(int i=0; i<d; i++) pw *= digit; // integer pow, no float
sum += pw;
}
return sum == n;
}
void armstrongRange(int L, int R){
for(int i=L; i<=R; i++)
if(isArmstrong(i)) cout << i << " ";
cout << "\n";
}
// 3-digit: 153, 370, 371, 407 | 4-digit: 1634, 8208, 9474Time: O((R−L)×d) | Space: O(1). Use integer multiplication instead of pow(digit, d) — pow uses floating-point and can return 152.9999... instead of 153, breaking the equality check.
Given n and k, find the integer k-th root of n — i.e., find r such that r^k = n. Return −1 if n is not a perfect k-th power.
lo=1, hi=125
mid=63: 63³=250047 > 125 → hi=62
mid=31: 31³=29791 > 125 → hi=30
mid=15: 15³=3375 > 125 → hi=14
mid= 7: 7³=343 > 125 → hi= 6
mid= 3: 3³= 27 < 125 → lo= 4
mid= 5: 5³=125 ==125 → Answer: 5int nthRoot(int n, int k){
long long lo=1, hi=n;
while(lo <= hi){
long long mid = lo + (hi-lo)/2;
long long pw = 1;
for(int i=0; i<k; i++){
pw *= mid;
if(pw > n){ pw = n+1; break; } // overflow guard
}
if(pw == n) return (int)mid;
if(pw < n) lo = mid+1;
else hi = mid-1;
}
return -1; // not a perfect k-th power
}Time: O(k × log n) | Space: O(1). The early-exit overflow guard (if pw > n: break) is critical — without it, large mid values cause pw to overflow long long silently.
Find the minimum number of steps to reduce n to 1. At each step you may: divide by 3 (if divisible), divide by 2 (if divisible), or subtract 1.
Greedy (always prefer ÷3 > ÷2 > −1) fails on some inputs. Example: n=10. Greedy: 10→9→3→1 = 3 steps. But DP gives the same here; on other inputs greedy diverges. DP guarantees the optimal choice by building up from n=1.
dp[1]=0
dp[2]=min(dp[1]+1) = 1 (÷2)
dp[3]=min(dp[2]+1, dp[1]+1) = 1 (÷3)
dp[4]=min(dp[3]+1, dp[2]+1) = 2
dp[5]=min(dp[4]+1) = 3 (−1→4→2→1)
dp[6]=min(dp[5]+1,dp[3]+1,dp[2]+1)= 2 (÷3 or ÷2)
dp[9]=min(dp[8]+1,dp[3]+1) = 2 (÷3)
dp[10]=min(dp[9]+1,dp[5]+1) = 3 (−1→9→3→1)int minSteps(int n){
vector<int> dp(n+1, INT_MAX);
dp[1] = 0;
for(int i=2; i<=n; i++){
dp[i] = dp[i-1] + 1; // subtract 1
if(i%2==0) dp[i] = min(dp[i], dp[i/2]+1); // divide by 2
if(i%3==0) dp[i] = min(dp[i], dp[i/3]+1); // divide by 3
}
return dp[n];
}
// n=1→0, n=2→1, n=3→1, n=10→3, n=15→4Time: O(n) | Space: O(n). This is a classic intro-DP problem. The key insight: build solutions bottom-up. Every sub-problem dp[i] needs dp[i/2] and dp[i/3] — those are smaller indices, already computed.
Convert an integer (1–3999) to its Roman numeral representation. Example: 2024 → "MMXXIV".
Maintain a list of value-symbol pairs in descending order, including the six subtractive forms (IV=4, IX=9, XL=40, XC=90, CD=400, CM=900). Greedily subtract the largest value that fits, appending its symbol.
2024 ≥ 1000 → append "M", n=1024
1024 ≥ 1000 → append "M", n= 24
24 ≥ 10 → append "X", n= 14
14 ≥ 10 → append "X", n= 4
4 ≥ 4 → append "IV", n= 0
Result: "MMXXIV"string toRoman(int n){
vector<pair<int,string>> vals = {
{1000,"M"}, {900,"CM"}, {500,"D"}, {400,"CD"},
{ 100,"C"}, { 90,"XC"}, { 50,"L"}, { 40,"XL"},
{ 10,"X"}, { 9,"IX"}, { 5,"V"}, { 4,"IV"},
{ 1,"I"}
};
string res;
for(auto& [v, s] : vals)
while(n >= v){ res += s; n -= v; }
return res;
}Time: O(1) — bounded by constant 13 entries × max 3999 iterations = bounded | Space: O(1). The subtractive forms (IV, IX…) in the table are the elegant trick — they turn an exception into a regular case.
Rotate an n×n matrix 90° clockwise in-place, using O(1) extra space.
Step 1 — Transpose: Swap m[i][j] with m[j][i] for all i < j. This flips the matrix along the main diagonal.
Step 2 — Reverse each row: Reversing each row after the transpose is equivalent to a 90° clockwise rotation.
After transpose: [[1,4,7],[2,5,8],[3,6,9]]
After reverse rows: [[7,4,1],[8,5,2],[9,6,3]]
90° clockwise ✓
Verify: top-left 1 → should move to top-right ✓
bottom-left 7 → should move to top-left ✓void rotate90(vector<vector<int>>& m){
int n = m.size();
// Step 1: Transpose (swap across main diagonal)
for(int i=0; i<n; i++)
for(int j=i+1; j<n; j++)
swap(m[i][j], m[j][i]);
// Step 2: Reverse each row
for(int i=0; i<n; i++)
reverse(m[i].begin(), m[i].end());
}Time: O(n²) | Space: O(1). For 90° anti-clockwise: reverse each row first, then transpose. For 180°: reverse the entire matrix (reverse rows, then reverse each row).
Find the element that appears more than n/2 times (guaranteed to exist). O(n) time, O(1) space.
Maintain a candidate and a counter. When count reaches 0, the current element becomes the new candidate. When the current element matches the candidate, increment; otherwise decrement. The majority element can never be fully "cancelled out" because it appears more than all others combined.
x=3: cnt=0 → cand=3, cnt=1
x=3: match → cnt=2
x=4: diff → cnt=1
x=2: diff → cnt=0
x=3: cnt=0 → cand=3, cnt=1
x=3: match → cnt=2
x=3: match → cnt=3
Candidate: 3 ✓ (appears 5/7 times > n/2)int majorityElement(vector<int>& a){
int cand = a[0], cnt = 1;
for(int i=1; i<(int)a.size(); i++){
if (cnt == 0) { cand = a[i]; cnt = 1; }
else if(a[i] == cand) cnt++;
else cnt--;
}
return cand; // guaranteed to exist, no verification needed
}Time: O(n) | Space: O(1). If existence is not guaranteed, add a second pass to verify the candidate actually appears >n/2 times. See P132 for the n/3 generalisation.
Find the length of the longest subarray whose elements sum to exactly k. The array may contain negative numbers.
[1,-1,5,-2,3], k=34 (subarray [1,-1,5,-2])If prefixSum[i] − prefixSum[j] = k, then subarray (j, i] has sum k. We want to maximise i − j. Store only the first occurrence of each prefix sum in a hash map. At each index i, check if prefixSum − k was seen earlier.
Map starts: {0: -1} (psum=0 at index "before array")
i=0: psum=1, need 1-3=-2, not in map. Add {1:0}
i=1: psum=0, need 0-3=-3, not in map. (0 already in map, skip)
i=2: psum=5, need 5-3= 2, not in map. Add {5:2}
i=3: psum=3, need 3-3= 0, 0 in map at -1 → len=3-(-1)=4. Add {3:3}
i=4: psum=6, need 6-3= 3, 3 in map at 3 → len=4-3=1
Max length = 4 (subarray indices 0..3: [1,-1,5,-2] sum=3 ✓)int longestSubarraySum(vector<int>& a, int k){
unordered_map<long long, int> first;
first[0] = -1; // prefix sum 0 exists before index 0
long long psum = 0;
int maxLen = 0;
for(int i=0; i<(int)a.size(); i++){
psum += a[i];
if(first.count(psum - k))
maxLen = max(maxLen, i - first[psum - k]);
if(!first.count(psum)) // store FIRST occurrence only
first[psum] = i;
}
return maxLen;
}Time: O(n) | Space: O(n). Storing only the first occurrence maximises the subarray length. If you also stored later occurrences, you'd get shorter subarrays.
Given an array of bar heights representing an elevation map (width 1 each), compute the total water trapped after raining.
[0,1,0,2,1,0,1,3,2,1,2,1]6 unitsWater above bar i = min(maxLeft, maxRight) − height[i], where maxLeft and maxRight are the tallest bars to the left and right of i respectively. If this is negative, no water sits at i.
Brute force scans left and right for every bar: O(n²). Better: precompute prefix max arrays: O(n) time, O(n) space. Optimal: two-pointer O(n) time, O(1) space — when left max < right max, the bottleneck is on the left; process and advance left pointer.
l=0 r=4 maxL=0 maxR=0
h[l]=3 > h[r]=2 → process right: h[4]=2 ≥ maxR=0 → maxR=2, r=3
h[l]=3 > h[r]=0 → process right: h[3]=0 < maxR=2 → water+=2-0=2, r=2
h[l]=3 > h[r]=1 → process right: h[2]=1 < maxR=2 → water+=2-1=1, r=1
h[l]=3 > h[r]=0 → process right: h[1]=0 < maxR=2 → water+=2-0=2, r=0
l≥r → done. Total = 5 unitsint trapRainWater(vector<int>& h){
int l=0, r=(int)h.size()-1;
int maxL=0, maxR=0, water=0;
while(l < r){
if(h[l] <= h[r]){
if(h[l] >= maxL) maxL = h[l];
else water += maxL - h[l];
l++;
} else {
if(h[r] >= maxR) maxR = h[r];
else water += maxR - h[r];
r--;
}
}
return water;
}Time: O(n) | Space: O(1). The two-pointer works because: whichever side has the smaller max is the binding constraint. We don't need to know the other side's exact max — we already know our side's max is smaller, so water = ourMax − height.
A class StringBox dynamically allocates a C-string. Show how the compiler-generated copy causes a double-free crash (shallow copy), and fix it with the Rule of Three: Destructor + Copy Constructor + Copy Assignment Operator.
The default copy constructor copies the pointer value — both objects then point to the same memory. When both are destroyed, delete[] fires twice on the same address: undefined behaviour (crash). The fix: in the copy constructor and assignment operator, allocate new memory and copy the contents.
Shallow: ObjA[ptr] ──┐
└──▶ "Hello" ←── ObjB[ptr]
delete ObjA → frees "Hello"
delete ObjB → CRASH: double free!
Deep: ObjA[ptr] ──▶ "Hello" (addr 0x10)
ObjB[ptr] ──▶ "Hello" (addr 0x20) (own copy)
Both delete safely ✓StringBox a("Hello") → allocates 6 bytes at 0x10
StringBox b = a → Deep: allocates 6 bytes at 0x20, copies chars
~b() → deletes 0x20 (OK)
~a() → deletes 0x10 (OK)class StringBox {
char* data;
public:
// Constructor
StringBox(const char* str){
data = new char[strlen(str)+1];
strcpy(data, str);
}
// Destructor
~StringBox(){ delete[] data; }
// Copy Constructor (deep)
StringBox(const StringBox& o){
data = new char[strlen(o.data)+1];
strcpy(data, o.data);
}
// Copy Assignment Operator (deep)
StringBox& operator=(const StringBox& o){
if(this == &o) return *this; // self-assignment guard
delete[] data;
data = new char[strlen(o.data)+1];
strcpy(data, o.data);
return *this;
}
};The self-assignment check if(this == &other) in the assignment operator is mandatory. Without it, obj = obj would delete obj's data before trying to copy from it — you'd copy garbage and crash.
Extend the Rule of Three with move semantics (C++11). Implement a Buffer class with a Move Constructor and Move Assignment Operator that "steal" the resource pointer instead of copying the whole array.
Returning a large object from a function normally triggers a deep copy: O(n) allocation. Since the temporary (rvalue) is about to be destroyed anyway, we can steal its pointer in O(1) — set our pointer to it, then null out the source so its destructor does nothing.
Copy: Allocate new block. Copy 1M elements. Slow — OS call! Move: target.ptr = source.ptr (1 assignment) source.ptr = nullptr (1 assignment) O(1) — instant steal!
class Buffer {
int* arr; int size;
public:
Buffer(int s) : size(s), arr(new int[s]) {}
~Buffer(){ delete[] arr; }
// Copy Constructor (deep)
Buffer(const Buffer& o) : size(o.size), arr(new int[o.size]){
for(int i=0;i<size;i++) arr[i]=o.arr[i];
}
// Move Constructor — steal the pointer
Buffer(Buffer&& o) noexcept : arr(o.arr), size(o.size){
o.arr = nullptr; // neutralize source
o.size = 0;
}
// Move Assignment Operator
Buffer& operator=(Buffer&& o) noexcept{
if(this != &o){
delete[] arr; // free current resource
arr = o.arr; size = o.size;
o.arr = nullptr; o.size = 0;
}
return *this;
}
// Mark copy/move assignment explicitly
Buffer& operator=(const Buffer&) = delete;
};Time: O(1) move, O(n) copy | Space: O(n). Always mark move operations noexcept — STL containers use move operations only if they are guaranteed not to throw.
Solve the diamond problem: Printer and Scanner both inherit from Device. Copier inherits from both. Without virtual inheritance, Copier has two copies of Device, causing ambiguity.
Add virtual to the base class inheritance in intermediate classes. The compiler then creates a single shared Device sub-object. The most-derived class (Copier) becomes responsible for initialising the virtual base directly in its initialiser list.
Without virtual: With virtual: [Device A] [vbtptr (Printer)] ─┐ [Printer] [Printer data ] │ share one Device [Device B] [vbtptr (Scanner)] ─┘ [Scanner] [Scanner data ] [Copier] [Copier data ] AMBIGUOUS [Shared Device ] ← single instance ✓
Without virtual: Device() → Printer() → Device() → Scanner() → Copier() (2×Device!) With virtual: Device() → Printer() → Scanner() → Copier() (1×Device ✓)
class Device {
public:
int voltage;
Device(int v) : voltage(v) {}
void powerOn(){ cout << "On at " << voltage << "V\n"; }
};
class Printer : virtual public Device {
public: Printer() : Device(110) {}
};
class Scanner : virtual public Device {
public: Scanner() : Device(220) {}
};
class Copier : public Printer, public Scanner {
public:
// Most-derived class initialises virtual base directly
Copier() : Device(110), Printer(), Scanner() {}
};Virtual inheritance adds a pointer (vbtptr) to each intermediate class — a tiny constant overhead per object. It also makes the constructor initialiser list order more subtle. Only use it when you genuinely need the diamond structure.
Show what happens when a derived object is deleted through a base pointer without a virtual destructor. Fix the memory leak with a virtual destructor.
Base* p = new Derived(); delete p; — without virtual ~Base(), the compiler statically resolves the destructor to ~Base(). ~Derived() is never called. Making ~Base() virtual forces dynamic dispatch through the vtable, ensuring ~Derived() runs first, then chains to ~Base().
Without virtual: delete p → ~Base() ~Derived() skipped → leak! With virtual: delete p → vtable → ~Derived() → ~Base() clean ✓
class Base {
public:
Base(){ cout << "Base created\n"; }
virtual ~Base(){ cout << "Base destroyed\n"; }
};
class Derived : public Base {
int* data;
public:
Derived(){ data = new int[1000]; cout << "Derived created\n"; }
~Derived() override{
delete[] data;
cout << "Derived destroyed\n";
}
};
int main(){
Base* p = new Derived();
delete p; // safe via virtual dispatch
}If a class has at least one virtual function, give it a virtual destructor. This is not optional — deleting a derived object through a base pointer without it is undefined behaviour in C++.
Create a Vector2D struct. Overload + (vector addition), * (dot product), and << (stream output) so that cout << A + B and A * B work naturally.
A + B maps to A.operator+(B). Binary operators that produce a new value return by value. The stream operator << must be a global friend function because the left operand is ostream, not our class.
C = A + B → [2+4, 3+1] = [6, 4] S = A * B → 2×4 + 3×1 = 11 (dot product)
struct Vector2D {
float x, y;
Vector2D(float x=0, float y=0) : x(x), y(y) {}
// + returns a new vector
Vector2D operator+(const Vector2D& o) const{
return {x+o.x, y+o.y};
}
// * returns scalar dot product
float operator*(const Vector2D& o) const{
return x*o.x + y*o.y;
}
// << must be a friend: left operand is ostream
friend ostream& operator<<(ostream& os, const Vector2D& v){
os << "[" << v.x << ", " << v.y << "]";
return os; // return os for chaining
}
};Time: O(1) | Space: O(1). Return os from operator<< to enable chaining: cout << v1 << v2. The const qualifier on member operators means they can be called on const objects.
Create a Multiplier class that overloads operator(). Instances of this class can be "called" like functions and remember their multiplier between calls — state that a plain function cannot have.
A plain function has no memory between calls. A functor stores state in member variables and exposes it through operator(). This is exactly how C++ lambdas are implemented under the hood — they are anonymous functor classes.
M(10) → operator()(10) → return 10 × 3 = 30 M(20) → operator()(20) → return 20 × 3 = 60 State (factor=3) persists between calls
class Multiplier {
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int value) const{
return value * factor;
}
};
int main(){
Multiplier times3(3);
cout << times3(10) << "\n"; // 30
cout << times3(20) << "\n"; // 60
// Plug into STL algorithms just like a lambda
vector<int> v = {1, 2, 3};
transform(v.begin(), v.end(), v.begin(), Multiplier(5));
// v is now {5, 10, 15}
}Time: O(1) per call | Space: O(1). STL algorithms like sort, transform, and find_if accept any callable — function pointer, functor, or lambda. Functors with state are the foundation for customisable comparators.
Build a minimal ScopedPointer<T> that wraps a raw pointer and guarantees deletion when it goes out of scope — even if an exception is thrown. This is RAII (Resource Acquisition Is Initialisation).
Tie the lifetime of a resource to the lifetime of a stack object. The destructor always runs when the stack frame is exited — normally or via exception. Wrap the raw pointer in a class whose destructor calls delete. Disable copying to make it "unique".
{
ScopedPointer<int> sp(new int(42));
cout << *sp; // prints 42
// Exception thrown here — scope still exits cleanly
} ← ~ScopedPointer() fires: delete ptr → no leak ✓template<typename T>
class ScopedPointer {
T* ptr;
public:
explicit ScopedPointer(T* p = nullptr) : ptr(p) {}
~ScopedPointer(){ delete ptr; }
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
// Disable copying — makes this pointer "unique"
ScopedPointer(const ScopedPointer&) = delete;
ScopedPointer& operator=(const ScopedPointer&) = delete;
};Deleting the copy operations is not optional — without it, two ScopedPointers can point to the same memory and both try to delete it (double-free). In real code use std::unique_ptr; this exercise exists to understand what unique_ptr is built from.
A BankVault class has private ledgers. Only a designated Auditor class should have read access to them for compliance checks — all other classes must be denied.
Inside the class granting access, declare friend class Auditor;. The Auditor then has full access to all private and protected members. Friendship is not inherited and not transitive — Auditor's subclasses do not automatically get access.
Auditor a; a.audit(vault) → accesses vault.secretCode Allowed — is friend ✓ OtherClass o; o.hack(vault) → accesses vault.secretCode Compiler Error: private ✗
class BankVault {
double totalCash = 1000000.0;
int vaultCode = 90210;
friend class Auditor; // only Auditor bypasses encapsulation
public:
void deposit(double amt){ totalCash += amt; }
};
class Auditor {
public:
bool audit(const BankVault& v){
cout << "Code: " << v.vaultCode << "\n"; // valid access
return v.totalCash >= 500000.0;
}
};Time: O(1) | Space: O(0) runtime. friend is a compile-time mechanism — zero runtime cost. Use it sparingly; overuse breaks encapsulation and makes code harder to reason about.
this Pointer Fluent APIBuild a QueryBuilder that supports fluent method chaining: qb.select("*").from("users").where("age>18").print().
Every method returns a reference to the current object (*this). Because the return type is QueryBuilder&, the caller can immediately chain another method call. No copies are made — all operations accumulate on the same object.
qb.select("*") → query = "SELECT * ", returns qb&
.from("users") → query += "FROM users ", returns qb&
.where("age>18") → query += "WHERE age>18;", returns qb&
.print() → prints "SELECT * FROM users WHERE age>18;"class QueryBuilder {
string query;
public:
QueryBuilder& select(string fields){
query = "SELECT " + fields + " ";
return *this;
}
QueryBuilder& from(string table){
query += "FROM " + table + " ";
return *this;
}
QueryBuilder& where(string cond){
query += "WHERE " + cond + ";";
return *this;
}
void print(){ cout << query << "\n"; }
};Time: O(n) string append | Space: O(n). This pattern — return *this by reference — is the entire basis of the Builder design pattern, stream operators (cout << a << b), and many modern fluent APIs.
Design an Entity class that accurately tracks the number of active instances at any moment using a static member.
An instance member belongs to each object. A static member belongs to the class — one copy shared across all instances. Incrementing in the constructor and decrementing in the destructor gives exact lifetime tracking.
Entity e1; count = 1
{
Entity e2, e3; count = 3
} // e2, e3 destroyed → count = 1
e1 destroyed → count = 0class Entity {
static int count; // one copy for the whole class
public:
Entity(){ count++; }
~Entity(){ count--; }
static int getCount(){ return count; }
};
// MUST be defined (not just declared) outside the class:
int Entity::count = 0;
int main(){
Entity e1;
{ Entity e2, e3; cout << Entity::getCount(); } // 3
cout << Entity::getCount(); // 1
}Declaring static int count; inside the class is only a declaration. Without int Entity::count = 0; in a .cpp file, you get a linker error: "undefined reference to Entity::count". Static members live outside any particular object — they need their own definition.
explicit Constructors StrictnessPrevent C++ from silently constructing objects from unintended implicit type conversions using the explicit keyword.
A single-argument constructor doubles as a conversion constructor. Without explicit, passing an int where a Vector is expected compiles silently — C++ constructs a temporary Vector from the int. This is a footgun. explicit disables this conversion entirely.
void draw(Vector v); Without: draw(15) → C++ silently creates Vector(15) ← implicit bug! With: draw(15) → Compile error: no implicit conversion ← safe! draw(Vector(15)) → Explicit — fine
class Vector {
int size;
public:
explicit Vector(int s) : size(s) {}
};
void draw(Vector v){ /* ... */ }
int main(){
// Vector v = 5; ERROR: implicit conversion forbidden
Vector v(5); // OK: direct construction
// draw(5); ERROR: no implicit conversion
draw(Vector(5)); // OK: explicit
}Time: O(1) | Space: O(1). Make it a habit: apply explicit to every single-argument constructor unless you specifically want the implicit conversion. The standard library uses it on std::string, std::vector, etc.
Design an AppConfig class where the entire program can only ever create exactly one instance. This is the Singleton pattern.
Hide the constructor (make it private). Expose a static get() method that returns a reference to a static local variable. C++11 guarantees static local variables are thread-safe on first initialisation. Delete copy and assignment operators to prevent duplication.
AppConfig::get() → first call: static instance created, constructor fires
AppConfig::get() → second call: static instance already exists, returned immediately
AppConfig::get() → third call: same instance, no construction
All three calls return a reference to the exact same objectclass AppConfig {
AppConfig(){ cout << "Config init\n"; } // private
public:
AppConfig(const AppConfig&) = delete;
AppConfig& operator=(const AppConfig&) = delete;
static AppConfig& get(){
static AppConfig instance; // created once, thread-safe (C++11)
return instance;
}
void load(){ cout << "Loading config...\n"; }
};
// Usage: AppConfig::get().load();A Singleton is effectively a global variable with a fancier name. It makes unit testing hard (you can't swap it out for a mock easily) and creates hidden coupling. Use it only when single-instance control is truly mandatory — audio drivers, config managers, loggers.
new & Memory Pools Extreme C++Construct objects in pre-allocated memory without OS interaction. This is the technique behind game engine object pools and embedded systems.
Standard new asks the OS for memory on every call — causing unpredictable latency spikes in real-time applications. A memory pool pre-allocates a large byte array once at startup. Placement new constructs objects at specific addresses inside that pool, bypassing the OS entirely.
Standard: Allocate → OS request → wait → get block → construct
Placement: Pool pre-allocated at startup
new(pool + offset) Enemy() → construct at exact address instantlychar pool[sizeof(Enemy)*10]; // pre-allocate (no OS call) Enemy* e = new(pool) Enemy(100); // construct in pool slot 0 e->~Enemy(); // manually call destructor // DO NOT delete e — pool owns the memory, not e delete[] pool; // release pool at shutdown
#include <new>
class Enemy {
int hp;
public:
Enemy(int h) : hp(h){ cout << "Spawned hp=" << hp << "\n"; }
~Enemy(){ cout << "Enemy destroyed\n"; }
void attack(){ cout << "Attack! hp=" << hp << "\n"; }
};
int main(){
// 1. Allocate raw byte pool — constructors NOT called
char* pool = new char[sizeof(Enemy) * 10];
// 2. Construct Enemy at pool slot 0 — no OS call
Enemy* e = new(pool) Enemy(100);
e->attack();
// 3. Manually destroy — do NOT use delete e!
e->~Enemy();
// 4. Release the pool block itself
delete[] pool;
}Never call delete e on a placement-newed pointer — it attempts to free memory back to the OS that the OS never gave you, corrupting the heap. Always call the destructor manually first, then free the pool separately.