Part 3 of 4 · Ch 7–9 · P62–P92
C++ Mastery · Complete Edition

Matrix · Intermediate DSA
Advanced OOP & Memory

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.

Ch 7 · Matrix Problems · P62–P66 Ch 8 · Intermediate Challenges · P67–P78 Ch 9 · Advanced OOP & Memory · P79–P92
Chapter 07
Matrix Problems — 2D Arrays, Diagonals & Transformations

P62 Sum of All Matrix Elements Beginner

Find the sum of all elements in a 2D matrix.

Input[[1,2],[3,4]]
Output10
Dry Run: [[1,2],[3,4]]
Row 0: 1 + 2 = 3
Row 1: 3 + 4 = 7
Total: 10
int matrixSum(vector<vector<int>>& m){
    int sum=0;
    for(auto& row : m)
        for(int x : row) sum += x;
    return sum;
}
Insight & Complexity

Time: O(R×C) | Space: O(1). The range-for over a 2D vector accesses rows by reference — no index arithmetic needed.

P63 Diagonal Sum & Difference Beginner

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.

Index Formula

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).

Matrix: [[1,2,3],[4,5,6],[7,8,9]]
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);
}
Insight & Complexity

Time: O(n) | Space: O(1). Single loop handles both diagonals simultaneously — the centre-subtraction guard is the key correctness detail.

P64 Identical Matrices Matrix Compare

Check if two matrices are identical: same dimensions and every corresponding element is equal.

A=[[1,2],[3,4]] B=[[1,2],[3,5]]
A[1][1]=4  !=  B[1][1]=5  →  Not identical
bool 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
}
Insight & Complexity

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.

P65 Toeplitz Matrix Matrix Property

A matrix is Toeplitz if every top-left-to-bottom-right diagonal has the same value. Check if a given matrix is Toeplitz.

Key Insight — Same Diagonal = Same (row − col)

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].

[[1,2,3],[4,1,2],[7,4,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;
}
Insight & Complexity

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).

P66 Number of Diagonals in a Polygon Formula

Find the number of diagonals in an n-sided polygon. A diagonal connects two non-adjacent vertices.

Formula Derivation

Total vertex pairs = C(n,2) = n(n−1)/2. Subtract the n sides (adjacent pairs). Result: n(n−3)/2.

Verify with known shapes
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)
Insight & Complexity

Time: O(1) | Space: O(1). Pattern: "count pairs, subtract forbidden pairs" — this combinatorial template solves dozens of counting problems.

↑ back to top
Chapter 08
Intermediate Challenges — Problems That Make You Think

P67 Automorphic Number Number Theory

A number n is automorphic if its square ends with the digits of n. Examples: 5² = 25 ✓, 76² = 5776 ✓, 376² = 141376 ✓.

Approach

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.

n = 76
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;
}
Insight & Complexity

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.

P68 Is a Power of Another Number? Math

Given a and b (b ≥ 2), check whether a = b^k for some integer k ≥ 0.

Inputa=27, b=3
OutputYes (3³)
Approach — Repeated Division

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.

a=27, b=3
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)
Insight & Complexity

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.

P69 Nearest Power of 2 Bit Logic

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.

Approach — Bit Trick

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.

n = 13
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}
Insight & Complexity

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.

P70 Triangle of Odd Numbers Math Pattern

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³.

Triangle (n=4)
1                    →  1³ = 1
3  5                 →  2³ = 8
7  9  11             →  3³ = 27
13 15 17 19          →  4³ = 64
Verify row sums
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";
    }
}
Insight & Complexity

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.

P71 Integer Square Root (No sqrt()) Binary Search

Find ⌊√n⌋ — the floor integer square root of n — without using sqrt().

Approach — Binary Search on Answer

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.

n = 35
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;
}
Insight & Complexity

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.

P72 Armstrong Numbers in a Range Number Theory

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³.

n = 153
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, 9474
Insight & Complexity

Time: 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.

P73 Integer k-th Root (Binary Search) Binary Search

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.

n=125, k=3
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: 5
int 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
}
Insight & Complexity

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.

P74 Minimum Steps to Reduce n to 1 Dynamic Programming

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.

Why DP, Not Greedy

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 table for n=10
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→4
Insight & Complexity

Time: 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.

P75 Integer to Roman Numeral Mapping + Greedy

Convert an integer (1–3999) to its Roman numeral representation. Example: 2024 → "MMXXIV".

Greedy — Subtract Largest Possible

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.

n = 2024
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;
}
Insight & Complexity

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.

P76 Rotate Matrix 90° In-Place Matrix Logic

Rotate an n×n matrix 90° clockwise in-place, using O(1) extra space.

Two-Step Approach

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.

[[1,2,3],[4,5,6],[7,8,9]]
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());
}
Insight & Complexity

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).

P77 Majority Element — Boyer-Moore Voting Smart Algorithm

Find the element that appears more than n/2 times (guaranteed to exist). O(n) time, O(1) space.

Boyer-Moore Voting Algorithm

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.

Array: [3, 3, 4, 2, 3, 3, 3]
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
}
Insight & Complexity

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.

P78 Longest Subarray with Given Sum Prefix Sum + Hash Map

Find the length of the longest subarray whose elements sum to exactly k. The array may contain negative numbers.

Input[1,-1,5,-2,3], k=3
Output4 (subarray [1,-1,5,-2])
Approach — Prefix Sum + First-Seen Hash Map

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.

Array: [1,-1,5,-2,3], k=3
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;
}
Insight & Complexity

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.

P79 Trapping Rain Water Two Pointer Mastery

Given an array of bar heights representing an elevation map (width 1 each), compute the total water trapped after raining.

Input[0,1,0,2,1,0,1,3,2,1,2,1]
Output6 units
The Core Insight

Water 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.

height=[3,0,1,0,2], optimal pass
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 units
int 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;
}
Insight & Complexity

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.

↑ back to top
Chapter 09
Advanced OOP & Memory — Mastering the Architecture

P80 Deep Copy vs. Shallow Copy — Rule of Three OOP

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 Problem and Fix

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.

Memory: Shallow vs Deep
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 ✓
Dry Run
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;
    }
};
Critical Gotcha

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.

P81 Rule of Five & Move Semantics Modern C++

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.

Why Move?

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.

Move vs Copy for 1,000,000-element array
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;
};
Insight & Complexity

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.

P82 Diamond Problem & Virtual Inheritance Architecture

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.

Virtual Inheritance

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.

Memory layout comparison
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 ✓
Constructor call order
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() {}
};
Gotcha

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.

P83 Virtual Destructors & Polymorphic Deletion Memory

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.

The vtable Mechanism

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

delete via 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
}
Rule

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

P84 Operator Overloading — Vector2D Syntax

Create a Vector2D struct. Overload + (vector addition), * (dot product), and << (stream output) so that cout << A + B and A * B work naturally.

Key Rules

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.

A=[2,3], B=[4,1]
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
    }
};
Insight & Complexity

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.

P85 Functors — Stateful Function Objects Core Concept

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.

Functor vs Function

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.

Multiplier M(3)
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}
}
Insight & Complexity

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.

P86 Custom Smart Pointer — RAII Memory

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).

RAII Principle

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".

Scope exit scenario
{
  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;
};
Critical

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.

P87 Friend Classes — Controlled Encapsulation Bypass Access Control

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.

The friend Keyword

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.

Access check
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;
    }
};
Insight & Complexity

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.

P88 Method Chaining via this Pointer Fluent API

Build a QueryBuilder that supports fluent method chaining: qb.select("*").from("users").where("age>18").print().

Key Mechanic

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.

Chain execution
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"; }
};
Insight & Complexity

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.

P89 Static Members & Instance Counting Class Global

Design an Entity class that accurately tracks the number of active instances at any moment using a static member.

Static Member vs Instance 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.

Scope tracking
Entity e1;            count = 1
{
  Entity e2, e3;      count = 3
}  // e2, e3 destroyed → count = 1
e1 destroyed → count = 0
class 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
}
Common Error

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.

P90 explicit Constructors Strictness

Prevent C++ from silently constructing objects from unintended implicit type conversions using the explicit keyword.

The Implicit Hazard

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.

Without vs With explicit
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
}
Insight & Complexity

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.

P91 Singleton Pattern — Meyers Singleton Architecture

Design an AppConfig class where the entire program can only ever create exactly one instance. This is the Singleton pattern.

Meyers Singleton

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.

Multiple calls to get()
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 object
class 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();
Trade-off

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.

P92 Placement 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.

Why Placement new?

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 new vs Placement new
Standard:  Allocate → OS request → wait → get block → construct
Placement: Pool pre-allocated at startup
           new(pool + offset) Enemy() → construct at exact address instantly
Pool lifecycle
char 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;
}
Critical Rule

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.

↑ back to top