Pointers Challenges (10)

← Module home · Attempt 30 minutes before reading any solution — and draw first: every challenge here is solved on paper before it compiles. Solutions separated at the end; each gives the approach plus key code. ★ = Lesson 1–2 · ★★ = adds Lesson 3 · ★★★ = combines everything.


C1 — The pointer swap (★)

Write void swapPtr(int* a, int* b) — swap two caller variables using only pointer syntax — and prove it with a three-case test: (a) two distinct variables (b) the same variable passed twice (swapPtr(&x, &x)) (c) a null passed — document what your function does.

C2 — The address-of-maximum walk (★★)

Given an int array, find the address of its maximum element using only pointer arithmetic — no [], no index variables. Return nullptr for an empty range. Then report the maximum’s position by subtracting: pos = maxPtr - arr — and explain why pointer subtraction yields a count.

C3 — The two-pointer partition (★★)

The arrays module’s stable partitioner used indices. Redo it with pointers: move all even numbers of an array to its front (order within each group preserved), walking with a read cursor and a write cursor. Report how many writes happened.

C4 — The dynamic roster (★★)

Read an unknown count of exam scores (sentinel −1), storing them in a dynamic array that grows by doubling (start capacity 4; when full, allocate double, copy, delete[] the old, re-point). Return both the handle and the final count through an out-parameter. Document the ownership contract in one comment line.

C5 — The leak detective (★★★)

Study this snippet and produce a written audit: every heap box, its owner at each line, and a final verdict (clean / leaked / dangling / double-delete):

int* a = new int(1);
int* b = a;
delete a;
a = nullptr;
b = new int(2);
delete[] b;

Then repair it to a clean version with the same behaviour.

C6 — The null-safe toolkit (★★)

Three functions, one contract style: countNeg, sumPos, firstZero — each takes (const int* a, int n), each treats nullptr or n <= 0 as “empty range” (returning 0 / 0 / nullptr respectively). Prove all six degenerate paths with a test driver.

C7 — The reverse-in-place (★★)

Reverse a dynamic array in place with two pointers walking from the ends, swapping through dereference — no second array, no indices. Then state, in one sentence, why the two pointers’ meeting condition is left < right and not left != right.

C8 — The ownership chain (★★★)

Design (on paper, then code) a tiny sequence: int* makeFilled(int n, int start) returns a heap array filled start, start+1, ...; int* append(int* base, int& baseN, int value) returns a new array one longer (old one’s fate: your documented choice — but no leaks, no dangling); main uses both and ends clean. Diagram the ownership at every handoff.

C9 — The const-propagation puzzle (★★★)

Given const int* data (size n), write int countAbove(const int* data, int n, int t) — and then explain each const in the full signature const int* countAbovePtr(const int* data, int n, int t, const int* limit) returning the address of the first element above *limit… or argue, in writing, why returning an address into const data forces the caller’s pointer to be const int* too — the const is contagious, and that’s a feature.

C10 — The family triage (★★★)

Five snippets, no running allowed — for each: family (leak/dangling/invalid/double-delete/clean), one-line reason, one-line fix:

(a) int* p = new int[4]; p[4] = 0; delete[] p; (b) int* f() { int* q = new int(3); return q; } — called as f(); with the result discarded (c) int* p = new int(1); int* q = p; delete p; delete q; (d) int* p = new int(1); delete p; p = nullptr; if (p) std::cout << *p; (e) void g(int* p) { *p = 5; } — called as g(nullptr);


Solutions (approaches + key code)

C1 — Pointer swap
void swapPtr(int* a, int* b) {
    if (a == nullptr || b == nullptr) return;   // documented: null = no-op
    if (a == b) return;                          // same box: swapping with itself is a no-op anyway
    int t = *a; *a = *b; *b = t;
}

(a) works — two arrows into two boxes, rotate the values. (b) the a == b guard makes it explicitly safe; even without it, the classic body is self-neutralizing (t = a; *a = *a; *a = t). (c) the null guard makes the call a no-op — write that in the contract comment. The whole point of the pointer version: it works where references can’t even *say “maybe”.

C2 — Address of maximum
const int* maxAddr(const int* arr, int n) {
    if (arr == nullptr || n <= 0) return nullptr;
    const int* best = arr;
    for (const int* p = arr + 1; p != arr + n; p = p + 1)   // begin+1 .. one-past-end
        if (*p > *best) best = p;
    return best;
}
// position:  int pos = maxAddr(arr, n) - arr;    // pointer difference = element count

Why subtraction yields a count: p - arr asks “how many strides between the two addresses?” — the compiler divides the byte distance by the type’s size. It’s the arithmetic twin of i - 0. (Lesson 3 §1)

C3 — Two-pointer partition
int evensFirst(int* a, int n) {
    int* write = a;                          // next slot for an even value
    int writes = 0;
    for (int* read = a; read != a + n; read = read + 1) {
        if (*read % 2 == 0) {
            int t = *write; *write = *read; *read = t;   // swap into the write slot
            write = write + 1;
            writes += 1;
        }
    }
    return writes;
}

read scans every box once; write lags behind, marking where the next even belongs. When read finds an even, swap it into *write and advance write. Order within evens is preserved because read moves forward only. Trace {3, 8, 5, 2}: read hits 8 → swap into slot 0 → {8, 3, 5, 2}; hits 2 → swap into slot 1 → {8, 2, 5, 3}; writes = 2 ✓.

C4 — Doubling roster
// Contract: caller owns the returned array and must delete[] it exactly once.
void readScores(int*& data, int& count) {      // data is an in/out HANDLE — see below
    int cap = 4;
    data  = new int[cap];
    count = 0;
    int v;
    std::cin >> v;
    while (v != -1) {
        if (count == cap) {                    // full: grow by doubling
            int* bigger = new int[cap * 2];
            for (int i = 0; i < count; i = i + 1) bigger[i] = data[i];
            delete[] data;                     // release the old block
            data = bigger;                     // re-point — old arrow replaced AFTER the delete
            cap  = cap * 2;
        }
        data[count] = v;
        count += 1;
        std::cin >> v;
    }
}

The subtle syntax: int*& data is a reference to a pointer — so the function can re-aim the caller’s arrow (Job 1 on a parameter!). Without the &, the doubling would re-aim a local copy and leak. Trace one growth: capacity 4 full at count 4 → allocate 8, copy 4, delete old 4-block, re-point, continue. Pairs stay balanced: every old block is deleted exactly when replaced. This is, in miniature, how std::vector grows — the course’s automation of exactly this.

C5 — Leak detective audit

Line-by-line ownership:

Line Heap boxes alive Owner(s) Verdict
1 box₁(1) a ok
2 box₁ a and b two arrows, one box — not yet a bug
3 — (box₁ freed) none ok — but b now dangles
4 — — a cured; b still dangling
5 box₂(2) b (re-point after old box deleted — no orphan here) ok
6 — — form mismatch: delete[] on a non-array new

Verdict: one form-mismatch (undefined behaviour) + a transient dangling window (harmless here because b was re-pointed before use, but a loaded gun on the floor). Clean repair:

int* a = new int(1);
delete a;          // release before losing the arrow — b never needed
a = nullptr;
int* b = new int(2);
delete b;          // matching form: new ↔ delete (no brackets!)
b = nullptr;

If both pointers must co-exist, the ownership rule says one owner deletes; the other’s arrow must be tamed (nulled or scoped away) before the delete happens.

C6 — Null-safe toolkit
int countNeg(const int* a, int n) {
    if (a == nullptr || n <= 0) return 0;      // the empty-range contract
    int c = 0;
    for (int i = 0; i < n; i = i + 1) if (a[i] < 0) c += 1;
    return c;
}
int sumPos(const int* a, int n) {
    if (a == nullptr || n <= 0) return 0;
    int s = 0;
    for (int i = 0; i < n; i = i + 1) if (a[i] > 0) s += a[i];
    return s;
}
const int* firstZero(const int* a, int n) {
    if (a == nullptr || n <= 0) return nullptr;
    for (int i = 0; i < n; i = i + 1) if (a[i] == 0) return &a[i];
    return nullptr;
}
// driver: run each with (nullptr, 5), (data, 0), (data, -3), (data, n) — 12 rows, all survive

The design idea: degenerate inputs get one guarded line, uniformly — then the body can assume reality. This is the guard-chain pattern (decisions module) applied to pointers.

C7 — Reverse in place
void reverse(int* a, int n) {
    int* left  = a;
    int* right = a + n - 1;            // last box
    while (left < right) {
        int t = *left; *left = *right; *right = t;
        left  = left + 1;
        right = right - 1;
    }
}

Why left < right: with left != right, on an even length the pointers cross and never compare equal — they swap past each other and keep swapping outward pairs a second time (the array un-reverses!), then walk off both ends into invalid memory. left < right makes the middle (and the cross) a clean stop. Trace {1,2,3,4}: swap (1,4) → swap (2,3) → left(5008) == right(5004) fails left<right? — precisely: after the second swap left points past right, so < is false → stop ✓. With != they’d pass through each other and re-swap (1,4) again.

C8 — Ownership chain
int* makeFilled(int n, int start) {
    int* a = new int[n];
    for (int i = 0; i < n; i = i + 1) a[i] = start + i;
    return a;                       // HANDOFF → caller owns
}

// Contract: consumes the caller's block and returns a REPLACEMENT handle.
// After the call, base's old block is FREED — the returned pointer is the only owner.
int* append(int* base, int& baseN, int value) {
    int* grown = new int[baseN + 1];
    for (int i = 0; i < baseN; i = i + 1) grown[i] = base[i];
    grown[baseN] = value;
    baseN = baseN + 1;
    delete[] base;                  // the transfer: old owner consumes itself
    return grown;
}

int main() {
    int n = 3;
    int* a = makeFilled(n, 10);     // main owns: 10 11 12
    a = append(a, n, 99);           // hand UP and catch the replacement — 10 11 12 99
    // ... use a ...
    delete[] a;                     // the single surviving owner releases
    a = nullptr;
}

Ownership diagram per handoff: makeFilled → arrow travels up, one owner. append → receives ownership in, allocates, releases the consumed block, hands the replacement up — at every instant exactly one arrow owns each live block. The dangerous alternative (append deletes nothing, caller must remember the old handle changed) is how leaks are born; “consume and replace” keeps it impossible to hold a stale arrow. (std::vector::push_back does this dance so you never see it.)

C9 — Const propagation
int countAbove(const int* data, int n, int t) {
    int c = 0;
    for (int i = 0; i < n; i = i + 1) if (data[i] > t) c += 1;
    return c;
}
const int* firstAbove(const int* data, int n, const int* limit) {
    if (limit == nullptr) return nullptr;
    for (int i = 0; i < n; i = i + 1)
        if (data[i] > *limit) return &data[i];
    return nullptr;
}

The const in const int* data promises this function won’t write through the handle. The return type const int* extends the same promise to the caller: “what I’m handing you is a window into data you must not modify (it may itself be const).” If it returned plain int*, the caller could *result = 0 — punching a hole in someone else’s const. The const is contagious on purpose: write-protection that survives the handoff. This is why Unit 09’s array parameters were const T arr[] all along — same rule, pre-existing the vocabulary.

C10 — Family triage

(a) invalid access — p[4] on a 4-box block (indices 0–3): out of bounds before a correct delete; fix the index. (b) leak — handle discarded; catch and own, or don’t allocate. (c) double delete — two owners; exactly one deletes, the other tames its arrow. (d) clean — the same-line idiom made the “just checking” honest (p is null, if (p) is false, no dereference). (e) invalid access — callee dereferences null; guard inside (if (p == nullptr) return;) or take a reference if “always exists” is the truth.