Chapter 19 — Dynamic Allocation: Project: DynBuffer Workbench
You are building the DynBuffer Workbench — a small library of five free
functions that manage a raw int array on the heap using new[] and
delete[]. No std::vector, no smart pointers, no malloc. Just you,
the heap, and a pointer.
The point is to feel the fragility firsthand: every new[] needs a matching
delete[], every function must leave no dangling address in the caller's
pointer variable, a deep copy means two independent heap allocations (not two
pointers to the same array), and "resize" means allocate-copy-delete-redirect.
Once you have done all of that by hand, you will understand exactly what
std::vector automates for you — and why LLVM's SmallVector exists.
This is also the lab where references to pointers (ch-12) pay off in a
visible way: destroyBuffer and resizeBuffer take int*& so they can write
nullptr or a new address back into the caller's variable. If they took
int* by value, the caller's pointer would never change.
Your tasks
makeBuffer(n, fill)— allocate a heap array ofnints withnew[], fill every element withfillin a loop, andreturnthe owning pointer. Precondition:n >= 0.destroyBuffer(p)— calldelete[]onp, then assignnullptrtopthrough the reference. Theint*&signature is what makes the write-back possible; without the&the caller's pointer would never see the change. Precondition:pis a valid owning pointer ornullptr.cloneBuffer(src, n)— deep copy: allocate a fresh array ofnints, copysrc[0..n-1]element-by-element, and return the new owning pointer. After cloneBuffer, mutating either array must NOT affect the other. Precondition:src != nullptrifn > 0.resizeBuffer(p, oldN, newN)— the four-step algorithm: (1)new[]a fresh array ofnewNints (value-init to 0). (2) Copymin(oldN, newN)elements from the old array into the new one. (3)delete[]the OLD array. (4) Setpto point at the new array through the reference. IfnewN > oldN, the extra slots are already 0. IfnewN < oldN, only the prefix survives. Order matters: step 2 must happen before step 3 because you read from the old allocation in step 2.bufferSum(buf, n)— loop overbuf[0..n-1]and return the sum. No allocation, no deallocation. Theconst int*parameter is the machine-readable promise that you won't modify the array.
Success criteria
makeBuffer(5, 7)— all five elements must be 7, not 0 (tests the fill loop)makeBuffer(0, 42)— length-0 allocation must not crash (legalnew int[0]{})destroyBuffer(buf)→buf == nullptr— the ref write-back must happendestroyBuffer(nullptr)— must be a no-op, not a crash- Clone independence: mutate the original after
cloneBuffer→ clone is unchanged (deep copy) - Shrink
resizeBuffer(buf, 5, 3)→ only the prefix of 3 elements survives - Grow
resizeBuffer(buf, 3, 6)→ new slots are 0, old prefix intact resizeBuffer(buf, 3, 0)→ shrink to 0;destroyBufferthen nulls the pointerbufferSumwith all-zeros (length 0), negatives, and mixed-sign arrays- Integration: clone → resize original → both arrays remain independent
Concepts practiced
new[]/delete[]— array form of dynamic allocation (notes 19.2)new/delete(scalar form, context) — distinct from array form; mixing them is UB (notes 19.2)- Dangling pointers — what happens after
delete[]if you don't null out (notes 19.1) - Memory leaks — losing the only owning pointer before cleanup (notes 19.1)
- Null-out-after-delete — the defensive habit that makes bugs detectable (notes 19.1)
- Deep copy vs shallow copy — two separate allocations vs one aliased pointer (notes 19.2)
- The resize algorithm — allocate-copy-delete-redirect (notes 19.2 "Resizing means allocating a new array")
- Owning vs observing pointers —
int*owns,const int*observes (notes 19.1) - Reused from ch-12: ref-to-pointer out-param (
int*&) — the mechanism that lets a function write back a new pointer to the caller's variable - Reused from ch-08:
forloops for array traversal; range bounds via a separateint nparameter - Reused from ch-02: header /
.cppsplit — declarations indynbuf.h, definitions indynbuf.cpp - CS6340 tie-in: reading LLVM C-style pointer+length signatures —
void foo(const int* buf, int n)— as exactly this contract
Constraints
Allowed:
new[]/delete[](the chapter's new tools)new/deletescalar form (allowed, but you don't need them here)forloops,intarithmetic,nullptrint*&(ref-to-pointer out-param, ch-12)std::vectorin your own test experiments only — not inside the library functions
Forbidden (not taught yet or defeats the point):
std::unique_ptr/std::shared_ptr(Chapter 22) — not yet, and they hide the pointmalloc/free/realloc— these are C-style allocators, not the C++newpath- Placement
new— beyond scope std::vectorinside the library functions — that defeats the whole exercise
Required idioms:
- Every
new[]has exactly one matchingdelete[] - Set pointers to
nullptrafterdelete[]through the ref-param where appropriate - Pass
n >= 0as a precondition; do not addif (n < 0)guards (out of scope) - Deep copy in
cloneBuffer: two independent heap allocations, not pointer aliasing
Build & run locally
make # compile-check starter/dynbuf.cpp (already works)
make test # grade your code -> RED until the TASK blocks are filled in
make solution # build + run the reference against the tests
make test-solution # proof the reference passes
make clean # remove build artifactsThe grader compiles tests/tests.cpp together with your starter/dynbuf.cpp
and runs it. There is no interactive driver (make run just prints a reminder)
because a raw-pointer library is not safely runnable without a real harness.
Hints
Task 1 — makeBuffer (allocate + fill)
int* buf { new int[n]{} }; // heap-allocate n ints, zero-init (notes 19.2)
for (int i { 0 }; i < n; ++i)
buf[i] = fill;
return buf;new int[n]{} value-initialises all elements to 0 (the {} is the key).
You overwrite them with fill in the loop. The caller gets the pointer and
owns the cleanup — make sure you document that expectation (the header already
does).
Task 2 — destroyBuffer (delete[] + null-out)
delete[] p; // release heap storage (notes 19.2)
p = nullptr; // null out through the ref — prevents dangling (notes 19.1)The int*& in the signature is what makes p = nullptr write into the
caller's variable. If the parameter were int* (by value), assigning
nullptr would only change a local copy — the caller's pointer would still
hold the freed address (a dangling pointer).
No if (p) guard needed: delete[] nullptr is defined as a no-op.
Task 3 — cloneBuffer (deep copy)
int* dst { new int[n]{} };
for (int i { 0 }; i < n; ++i)
dst[i] = src[i];
return dst;The classic mistake is int* dst { src } — that copies the pointer (a
shallow copy). Both src and dst now point at the same memory. The
first delete[] frees it; the second delete[] is a double-free — undefined
behaviour. The deep copy above allocates separate memory so the two arrays are
fully independent.
Task 4 — resizeBuffer (the four steps, ORDER matters)
int* fresh { new int[newN]{} }; // Step 1: new allocation
int copyCount { (oldN < newN) ? oldN : newN }; // min(oldN, newN)
for (int i { 0 }; i < copyCount; ++i)
fresh[i] = p[i]; // Step 2: copy from OLD p
delete[] p; // Step 3: free OLD allocation
p = fresh; // Step 4: redirect caller's ptrWhy the order matters: step 2 reads from p (the old array). If you do
step 3 before step 2, you read from freed memory — undefined behaviour. Save
the new pointer in fresh, copy, then delete the old, then redirect.
The (oldN < newN) ? oldN : newN idiom is min(oldN, newN) without including
<algorithm> — fine at this chapter level.
Task 5 — bufferSum (pure observer)
int total { 0 };
for (int i { 0 }; i < n; ++i)
total += buf[i];
return total;The const int* parameter is a compile-enforced promise that you won't write
through buf. If you accidentally write buf[i] = ..., the compiler will
reject it — that's the whole point of const on a pointer.
Stuck on a warning or a build error instead of a test failure?
- "unused parameter" — if your placeholder comments out a parameter name
(e.g.
int /*n*/), that suppresses the warning without changing behaviour. Once you write the real body, un-comment the name. - "deleting a pointer to incomplete type" — you called
delete(scalar) on something allocated withnew[](array). Usedelete[]. - "bus error" or segfault — an out-of-bounds access or a double-free. Check
that your
makeBufferreturns an array of sizen, not 0. - Clone independence test fails — you returned
srcdirectly instead of allocating a new array. See Task 3 hint.
Stretch goals
- Wrap the five functions into a class
DynBufferwith a constructor and a destructor that callsdelete[]automatically — that's RAII (notes 19.3), and it is exactly whatstd::vectordoes under the hood. (Needs Chapter 14/15.) - Add a copy constructor and copy-assignment operator to
DynBufferthat perform deep copies (notes 19.2). Without them, copying aDynBufferwould do a shallow copy of the pointer — a double-free waiting to happen. (Needs Chapter 14; the full "Rule of Three/Five" is Chapter 22.) - Try running
make testunder Valgrind (valgrind --leak-check=full ./tests/run) or with AddressSanitizer (-fsanitize=addressadded to CXXFLAGS). A correct solution reports zero leaks and zero invalid accesses. The starter'sresizeBufferplaceholder deliberately leaks — watch it show up. - Replace the free functions with a
std::vector<int>implementation and observe how all the bookkeeping disappears — that's the lesson, made concrete. - Add a
bufferFind(const int* buf, int n, int target)function that returns the index of the first occurrence oftarget, or-1if not found. (Pure observer, same pattern as Task 5; usesstd::findfrom Chapter 18 if you want to compare.)
// ============================================================================
// starter/dynbuf.cpp — the DynBuffer Workbench implementation (STARTER)
// ----------------------------------------------------------------------------
// Fill in the five TASK blocks below. Each maps 1:1 to a task in the README
// and to a declaration in ../dynbuf.h. The bodies currently return
// PLACEHOLDERS so the file compiles immediately — that is why `make test` is
// RED right now. Your job is to turn it GREEN by writing real new[]/delete[]
// code.
//
// make build compile-check your starter (already works)
// make test grade it -> RED until the TASK blocks are filled
// make test-solution run the grader against the reference
//
// IMPORTANT RULES FOR THIS LAB
// ──────────────────────────────────────────────────────────────────────────
// • Match every new[] with exactly one delete[] (not scalar delete).
// • Match every new with exactly one delete (not delete[]).
// • After delete[] a pointer, set it to nullptr through the ref param so
// callers can detect the freed state. (notes 19.1 dangling-pointer danger)
// • Do NOT use smart pointers (unique_ptr, shared_ptr) — those are Chapter 22.
// • Do NOT use malloc/free or placement new.
// • Do NOT use std::vector to implement the functions (that defeats the point).
// std::vector may appear in tests/ only.
// ============================================================================
#include "../dynbuf.h" // always include our own paired header first (ch-02 / 2.11)
// — the compiler checks our bodies match the contract.
// ─── TASK 1: makeBuffer — allocate and fill ──────────────────────────────────
// Allocate a heap array of `n` ints with new[], then loop over all n elements
// and set each one to `fill`. Return the owning pointer.
//
// Key terms: new[] allocates on the HEAP (free store, notes 19.1/19.2); the
// returned pointer is the only handle to that memory — if you lose it you have
// a MEMORY LEAK (notes 19.1). The caller is responsible for calling delete[]
// when done.
//
// Hint: int* buf { new int[n]{} }; value-initialises all elements to 0, then
// you overwrite them in a loop. OR new int[n] leaves them uninitialised and
// you set them in the loop — either is fine here.
//
// >>> YOUR CODE HERE <<<
//
int* makeBuffer(int n, int /*fill*/)
{
// Placeholder: allocates the right number of elements but leaves them
// zero-initialised instead of filling with `fill`.
// The allocation is correct (uses n); the fill loop is MISSING.
// Tests that check buf[i] == fill will fail; tests that check buf[i] == 0
// (the fill-with-0 case) will pass by accident — which is fine since the
// grader has the non-zero fill cases too. Replace with the real loop.
return new int[n]{};
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: destroyBuffer — delete[] and null out ───────────────────────────
// Call delete[] on `p`, then assign nullptr to `p` through the reference.
// That single write-back is the whole reason the parameter is `int*&` instead
// of `int*` — a ref-to-pointer lets us reach back into the caller's variable.
//
// Key term: DANGLING POINTER — after delete[] the raw address still sits in the
// caller's variable. It looks valid but points at freed memory. Setting it to
// nullptr makes any accidental use-after-free detectable as a null dereference
// instead of silent heap corruption. (notes 19.1)
//
// Don't add an if (p) guard — deleting nullptr is a no-op and harmless.
//
// >>> YOUR CODE HERE <<<
//
void destroyBuffer(int*& p)
{
// Placeholder: does nothing — a silent memory leak.
// Fill this in to actually call delete[] and null the pointer.
(void)p; // suppress unused-parameter warning until you write the real code
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: cloneBuffer — deep copy ─────────────────────────────────────────
// Allocate a brand-new array of `n` ints and copy every element from
// src[0..n-1] into it. Return the new owning pointer.
//
// "Deep copy" means two separate heap allocations. Copying the pointer (a
// "shallow copy") would give two owners sharing one array — the SECOND delete[]
// would be a double-free, causing undefined behaviour. (notes 19.2)
//
// Hint: allocate, then use a for loop:
// for (int i { 0 }; i < n; ++i) dst[i] = src[i];
//
// >>> YOUR CODE HERE <<<
//
int* cloneBuffer(const int* /*src*/, int n)
{
// Placeholder: returns a value-initialised zero array, ignoring src.
// All elements are 0, so a clone of a non-zero array will fail the tests.
return new int[n]{};
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: resizeBuffer — grow-or-shrink the allocation ────────────────────
// Implement the four-step resize sequence from notes 19.2:
// Step 1. Allocate a NEW array of `newN` ints (value-init to 0 with {}).
// Step 2. Copy min(oldN, newN) elements from `p` into the new array.
// Step 3. delete[] the OLD array.
// Step 4. Set `p` (through the ref) to the new array pointer.
//
// Careful with step 3: you must NOT use `p` after delete[]. Save the new
// pointer in a local first, then delete[] the old one, then reassign `p`.
// Doing it in the wrong order is a classic use-after-free bug.
//
// For the element-count: if newN > oldN, you copy oldN elements (the rest are
// already 0 from value-init). If newN < oldN, you copy only newN elements
// (the truncated tail is gone — you already new[]'d a shorter array).
//
// >>> YOUR CODE HERE <<<
//
void resizeBuffer(int*& p, int oldN, int newN)
{
// Placeholder: allocates the new array but neither copies the data nor
// deletes the old allocation — a memory leak plus missing copy.
// Replace this with the real four-step sequence.
int* fresh { new int[newN]{} };
(void)oldN;
p = fresh; // WRONG: the old `p` was never deleted (leak) and data not copied
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: bufferSum — read-only observer ──────────────────────────────────
// Loop over buf[0..n-1] and return the sum of all elements. No allocation.
// No deallocation. `buf` is const so you can't accidentally modify elements.
//
// Key term: OWNING vs OBSERVING pointers (notes 19.1): this function is an
// OBSERVER. It sees the data but has no responsibility for the lifetime of the
// array. The `const int*` in the parameter makes that intent machine-checkable.
//
// >>> YOUR CODE HERE <<<
//
int bufferSum(const int* /*buf*/, int /*n*/)
{
return 0; // placeholder — correct only for an all-zeros buffer
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// ============================================================================
// solution/dynbuf.cpp — reference IMPLEMENTATION of the DynBuffer Workbench
// ----------------------------------------------------------------------------
// One complete, correct, warning-clean implementation of ../dynbuf.h.
// Peek only after you have attempted starter/dynbuf.cpp — the learning is in
// writing the new[]/delete[] bookkeeping yourself, then comparing.
//
// Every new[] in this file has exactly one matching delete[]. There are no
// leaks, no double-frees, and no dangling pointers visible to callers.
// A Valgrind/ASan run on the test binary will report zero errors.
//
// CS6340 lens: this entire file is the skeleton of a resource-owning class.
// If you wrapped these functions into a class with a destructor, you'd have
// a handwritten RAII buffer — exactly what std::vector does (notes 19.3).
// ============================================================================
#include "../dynbuf.h" // paired-header include (ch-02 / 2.11): compile-time
// contract check — bodies must match declarations.
// ─── TASK 1: makeBuffer ──────────────────────────────────────────────────────
// new int[n] requests `n` consecutive ints from the HEAP (free store, notes
// 19.1 "The heap"; the array form of new is notes 19.2).
// The {} value-initialises them to 0; the loop then writes `fill`.
// (We could skip the value-init since we overwrite every element, but starting
// from 0 and overwriting is clearer and never leaks information.)
//
// Ownership: we allocate and IMMEDIATELY hand the pointer back to the caller.
// From this moment, the CALLER is the owner responsible for delete[].
int* makeBuffer(int n, int fill)
{
int* buf { new int[n]{} }; // heap-allocate n ints, zero-initialised (19.2)
for (int i { 0 }; i < n; ++i)
buf[i] = fill; // overwrite every slot with the requested value
return buf; // transfer ownership to the caller
}
// ─── TASK 2: destroyBuffer ───────────────────────────────────────────────────
// The `int*&` (reference to pointer) is what makes this an in-out parameter.
// Without the `&` the assignment `p = nullptr` would modify only a local copy
// of the pointer, and the caller's variable would still hold the dangling
// address — a classic silent-but-deadly bug (notes 19.1 dangling pointers,
// notes 12 ref-to-pointer out-param pattern).
//
// delete[] nullptr is explicitly harmless (C++ standard); no guard needed.
void destroyBuffer(int*& p)
{
delete[] p; // release the heap storage; calls no dtors (plain int array)
p = nullptr; // null out the caller's copy so it can't dangle (notes 19.1)
}
// ─── TASK 3: cloneBuffer ─────────────────────────────────────────────────────
// This is a DEEP copy: two independent heap allocations. After this function
// returns, mutating one array does NOT affect the other.
//
// Contrast with: `int* alias { src };` — that is a SHALLOW copy (alias copy).
// Two "owners" of the same memory means two future delete[]s — undefined
// behaviour: double-free (notes 19.2 "copying the pointer does not copy the
// elements").
int* cloneBuffer(const int* src, int n)
{
int* dst { new int[n]{} }; // fresh array, independent of `src` (19.2)
for (int i { 0 }; i < n; ++i)
dst[i] = src[i]; // element-by-element copy: a real deep copy
return dst; // caller owns this new allocation
}
// ─── TASK 4: resizeBuffer ────────────────────────────────────────────────────
// This is the exact algorithm std::vector uses when it grows (notes 19.2
// "Resizing means allocating a new array"). Order matters critically:
//
// WRONG order: delete[] p; then p = fresh;
// — After delete[] you have freed p's memory. Assigning p to fresh is fine,
// but you can no longer read old elements because the memory is gone.
// We already saved them above, so the COPY must happen BEFORE the delete[].
//
// RIGHT order (implemented below):
// 1. new[] — fresh allocation is independent; old allocation untouched.
// 2. copy — read from old allocation while it is still live.
// 3. delete[] — now it is safe to free the old allocation.
// 4. p = fresh — redirect caller's pointer to the new home.
void resizeBuffer(int*& p, int oldN, int newN)
{
// Step 1 — allocate the new, correctly-sized array, zero-initialised.
// Extra slots (when newN > oldN) are already 0 from the `{}`.
int* fresh { new int[newN]{} };
// Step 2 — copy the SMALLER of oldN and newN elements from the old array.
// If shrinking: we keep only the prefix.
// If growing: we keep everything and the new tail stays 0.
int copyCount { (oldN < newN) ? oldN : newN }; // min(oldN, newN)
for (int i { 0 }; i < copyCount; ++i)
fresh[i] = p[i]; // read from OLD allocation — it is still valid here
// Step 3 — free the OLD allocation. We've already extracted what we need.
delete[] p; // p is still the old pointer here — safe to delete
// Step 4 — redirect the caller's pointer to the new allocation.
p = fresh; // caller now owns `fresh`; old memory is released
}
// ─── TASK 5: bufferSum ───────────────────────────────────────────────────────
// A pure observer: reads but never modifies or owns. The `const int*` in the
// signature is the machine-readable promise that we will not write through `buf`
// (notes 12 const correctness; notes 19.1 observing vs owning pointers).
//
// No allocation, no deallocation. The only resource here is CPU time.
int bufferSum(const int* buf, int n)
{
int total { 0 };
for (int i { 0 }; i < n; ++i)
total += buf[i];
return total;
}
// ============================================================================
// tests/tests.cpp — automated grader for the DynBuffer Workbench (Ch 19)
// ----------------------------------------------------------------------------
// This file is the CONSUMER of dynbuf.h. It includes the contract header
// and calls every API function through a tiny no-framework harness. The
// Makefile compiles it against EITHER starter/dynbuf.cpp (`make test`) OR
// solution/dynbuf.cpp (`make test-solution`).
//
// IMPORTANT — LEAK-CLEAN BY CONSTRUCTION:
// Every pointer returned by makeBuffer / cloneBuffer / resizeBuffer is freed
// (via destroyBuffer or a direct delete[]) before the test exits. If your
// implementation is correct, there are zero unmatched allocations. If the
// starter placeholders are used, the leaked allocation from the placeholder
// resizeBuffer is detectable by ASan / Valgrind.
//
// Tiny harness — no external dependencies:
// CHECK(cond) — fails with condition text + line number; increments `fails`.
// The program exits 0 on full pass, 1 on any failure.
// ============================================================================
#include <iostream>
#include "../dynbuf.h"
static int fails { 0 };
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; \
++fails; \
} \
} while (0)
int main()
{
// ═════════════════════════════════════════════════════════════════════════
// TASK 1 — makeBuffer: allocate and fill
// ─────────────────────────────────────────────────────────────────────────
// Basic fill — normal case
{
int* buf { makeBuffer(5, 7) };
for (int i { 0 }; i < 5; ++i)
CHECK(buf[i] == 7); // every element must equal fill
delete[] buf;
}
// Fill with 0
{
int* buf { makeBuffer(3, 0) };
for (int i { 0 }; i < 3; ++i)
CHECK(buf[i] == 0);
delete[] buf;
}
// Fill with a negative value
{
int* buf { makeBuffer(4, -1) };
for (int i { 0 }; i < 4; ++i)
CHECK(buf[i] == -1);
delete[] buf;
}
// Edge case: length 1
{
int* buf { makeBuffer(1, 99) };
CHECK(buf[0] == 99);
delete[] buf;
}
// Edge case: length 0 — new int[0] is legal; we get a non-null pointer we
// must still delete[]. No elements to check, but must not crash.
{
int* buf { makeBuffer(0, 42) };
// buf != nullptr is guaranteed by the standard for new int[0]
delete[] buf;
}
// ═════════════════════════════════════════════════════════════════════════
// TASK 2 — destroyBuffer: delete[] and null out
// ─────────────────────────────────────────────────────────────────────────
// After destroyBuffer the pointer variable must be nullptr.
{
int* buf { makeBuffer(3, 5) };
destroyBuffer(buf);
CHECK(buf == nullptr); // the in-out ref must have written nullptr back
}
// Calling destroyBuffer on nullptr must be a no-op (not a crash).
// delete[] nullptr is defined to have no effect; the assignment is also fine.
{
int* p { nullptr };
destroyBuffer(p);
CHECK(p == nullptr);
}
// ═════════════════════════════════════════════════════════════════════════
// TASK 3 — cloneBuffer: deep copy
// ─────────────────────────────────────────────────────────────────────────
// Cloned array must have the same values as the original.
{
int* original { makeBuffer(4, 0) };
original[0] = 10; original[1] = 20; original[2] = 30; original[3] = 40;
int* clone { cloneBuffer(original, 4) };
CHECK(clone[0] == 10);
CHECK(clone[1] == 20);
CHECK(clone[2] == 30);
CHECK(clone[3] == 40);
// ── DEEP-COPY independence test ───────────────────────────────────
// Mutating the ORIGINAL must NOT affect the clone (and vice versa).
// If cloneBuffer just copied the pointer (shallow copy) this would fail.
original[0] = 999;
CHECK(clone[0] == 10); // clone is independent: still 10, not 999
clone[3] = -1;
CHECK(original[3] == 40); // original is independent: still 40
destroyBuffer(original);
destroyBuffer(clone);
}
// Clone of a length-0 buffer — legal edge case
{
int* empty { makeBuffer(0, 0) };
int* cloned { cloneBuffer(empty, 0) };
// Both are valid (non-null) heap pointers we must free.
destroyBuffer(empty);
destroyBuffer(cloned);
}
// ═════════════════════════════════════════════════════════════════════════
// TASK 4 — resizeBuffer: grow and shrink
// ─────────────────────────────────────────────────────────────────────────
// ── Growing: prefix is preserved, new slots are 0 ────────────────────
{
int* buf { makeBuffer(3, 0) };
buf[0] = 1; buf[1] = 2; buf[2] = 3;
resizeBuffer(buf, 3, 6); // grow from 3 to 6
// Prefix: original values intact
CHECK(buf[0] == 1);
CHECK(buf[1] == 2);
CHECK(buf[2] == 3);
// New slots: value-initialised to 0
CHECK(buf[3] == 0);
CHECK(buf[4] == 0);
CHECK(buf[5] == 0);
destroyBuffer(buf);
}
// ── Shrinking: keeps only the prefix ──────────────────────────────────
{
int* buf { makeBuffer(5, 0) };
buf[0] = 10; buf[1] = 20; buf[2] = 30; buf[3] = 40; buf[4] = 50;
resizeBuffer(buf, 5, 3); // shrink from 5 to 3
CHECK(buf[0] == 10);
CHECK(buf[1] == 20);
CHECK(buf[2] == 30);
// Elements at indices 3 and 4 are gone — we only have 3 elements now.
destroyBuffer(buf);
}
// ── Shrink to newN == 1 (extreme prefix) ──────────────────────────────
{
int* buf { makeBuffer(4, 0) };
buf[0] = 7; buf[1] = 8; buf[2] = 9; buf[3] = 10;
resizeBuffer(buf, 4, 1);
CHECK(buf[0] == 7); // only the first element survives
destroyBuffer(buf);
}
// ── Resize to same size — no-op semantically ──────────────────────────
{
int* buf { makeBuffer(3, 5) };
resizeBuffer(buf, 3, 3);
for (int i { 0 }; i < 3; ++i)
CHECK(buf[i] == 5);
destroyBuffer(buf);
}
// ── Grow from 0 ───────────────────────────────────────────────────────
{
int* buf { makeBuffer(0, 0) };
resizeBuffer(buf, 0, 4);
for (int i { 0 }; i < 4; ++i)
CHECK(buf[i] == 0); // all zero (value-init)
destroyBuffer(buf);
}
// ── Shrink to 0 ───────────────────────────────────────────────────────
{
int* buf { makeBuffer(3, 9) };
resizeBuffer(buf, 3, 0);
// buf now points to a valid (non-null) zero-length allocation
destroyBuffer(buf);
CHECK(buf == nullptr);
}
// ═════════════════════════════════════════════════════════════════════════
// TASK 5 — bufferSum: read-only observer
// ─────────────────────────────────────────────────────────────────────────
// Basic sum
{
int* buf { makeBuffer(4, 0) };
buf[0] = 1; buf[1] = 2; buf[2] = 3; buf[3] = 4;
CHECK(bufferSum(buf, 4) == 10);
destroyBuffer(buf);
}
// All-same fill
{
int* buf { makeBuffer(5, 3) };
CHECK(bufferSum(buf, 5) == 15);
destroyBuffer(buf);
}
// Negative values
{
int* buf { makeBuffer(3, 0) };
buf[0] = -1; buf[1] = -2; buf[2] = -3;
CHECK(bufferSum(buf, 3) == -6);
destroyBuffer(buf);
}
// Zero-length — sum of nothing is 0
{
int* buf { makeBuffer(0, 99) };
CHECK(bufferSum(buf, 0) == 0);
destroyBuffer(buf);
}
// Mixed sign
{
int* buf { makeBuffer(4, 0) };
buf[0] = 10; buf[1] = -3; buf[2] = 5; buf[3] = -2;
CHECK(bufferSum(buf, 4) == 10);
destroyBuffer(buf);
}
// ═════════════════════════════════════════════════════════════════════════
// INTEGRATION — chain of operations (mirrors real code that uses a buffer)
// ─────────────────────────────────────────────────────────────────────────
{
// Allocate, work, clone (deep copy), resize the original, verify
// independence throughout.
int* data { makeBuffer(3, 0) };
data[0] = 1; data[1] = 2; data[2] = 3;
int* snapshot { cloneBuffer(data, 3) }; // deep copy before resize
resizeBuffer(data, 3, 5); // grow; snapshot must be unaffected
data[3] = 4; data[4] = 5;
// snapshot still reflects the pre-resize values
CHECK(bufferSum(snapshot, 3) == 6); // 1+2+3
// data has the grown version
CHECK(bufferSum(data, 5) == 15); // 1+2+3+4+5
destroyBuffer(snapshot);
destroyBuffer(data);
}
// ═════════════════════════════════════════════════════════════════════════
// RESULT
// ─────────────────────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all dynbuf checks passed.\n";
else
std::cout << "FAIL \xE2\x9D\x8C " << fails
<< " check(s) failed — see lines above.\n";
return fails ? 1 : 0;
}
// ============================================================================
// dynbuf.h — the DynBuffer Workbench API (Chapter 19 — Dynamic Allocation)
// ----------------------------------------------------------------------------
// This header DECLARES five free functions that manage raw int arrays on the
// heap using new[] / delete[]. DO NOT EDIT THIS FILE — the grader
// (tests/tests.cpp) and both starter/dynbuf.cpp and solution/dynbuf.cpp
// include it. Changing a signature breaks the build.
//
// THE BIG IDEA OF THIS LAB
// ──────────────────────────────────────────────────────────────────────────
// Every competent C++ programmer needs to *understand* what std::vector does
// for you under the hood. This workbench builds each piece by hand:
//
// makeBuffer — new[] creates a heap array of any runtime-determined size.
// destroyBuffer— delete[] releases it; we null out the pointer through a
// reference (ch-12 ref-to-pointer paying off) so callers can't
// accidentally dereference the now-freed address.
// cloneBuffer — a DEEP copy: two independent arrays, not a pointer alias.
// resizeBuffer — the grow-and-move pattern that std::vector automates
// (and that LLVM's SmallVector uses internally).
// bufferSum — an observer: const pointer + length, no ownership change.
//
// CS6340 tie-in: every time you see an LLVM function like
// void processCounters(unsigned* counters, int n);
// you are staring at this same contract — a raw pointer carries no length and
// no ownership information; you must read the docs or the naming convention.
// The point of this lab is to feel that fragility in your hands.
//
// Header guard (ch-02 / 2.12):
// ============================================================================
#ifndef DYNBUF_H
#define DYNBUF_H
// ─── TASK 1 ──────────────────────────────────────────────────────────────────
// makeBuffer(n, fill) — allocate a heap array of `n` ints and set every
// element to `fill`. Returns the owning pointer.
//
// Precondition: n >= 0 (a zero-length dynamic array is technically valid;
// new int[0] returns a non-null pointer you must still delete[]).
//
// Ownership contract: the CALLER owns the returned pointer and must
// eventually call delete[] (or pass it to destroyBuffer).
int* makeBuffer(int n, int fill);
// ─── TASK 2 ──────────────────────────────────────────────────────────────────
// destroyBuffer(p) — delete[] the array pointed to by `p`, then SET `p` to
// nullptr through the reference. The ref-to-pointer signature (int*& p) lets
// this function WRITE BACK to the caller's pointer variable — the same
// mechanism as an out-parameter (ch-12 notes: references as in-out params).
//
// Nulling the pointer after delete is the single best defence against
// accidental use-after-free (dangling pointer danger, notes 19.1).
//
// Precondition: p is either a valid owning heap-array pointer OR nullptr.
// (Deleting nullptr is a no-op, and assigning nullptr to nullptr is harmless.)
void destroyBuffer(int*& p);
// ─── TASK 3 ──────────────────────────────────────────────────────────────────
// cloneBuffer(src, n) — deep-copy: allocate a fresh array of `n` ints and
// copy every element from src[0..n-1] into it. Returns the new owning
// pointer. Caller owns the returned pointer; `src` is not modified and is
// NOT transferred.
//
// This is a DEEP copy — after cloneBuffer the two arrays are independent.
// Changing one does NOT change the other (unlike copying a pointer, which
// gives you a SECOND owner pointing at the SAME memory — a double-free waiting
// to happen).
//
// Preconditions: src != nullptr if n > 0; n >= 0.
int* cloneBuffer(const int* src, int n);
// ─── TASK 4 ──────────────────────────────────────────────────────────────────
// resizeBuffer(p, oldN, newN) — reallocate to a different length:
// 1. new[] a fresh array of `newN` ints (value-initialized to 0).
// 2. Copy min(oldN, newN) elements from the old array into the new one.
// 3. delete[] the old array.
// 4. Set `p` (through the reference) to point at the new array.
//
// After resizeBuffer returns, the caller's pointer `p` refers to the new
// (possibly larger or smaller) array. If newN > oldN the extra slots are 0.
// If newN < oldN the prefix of min(oldN,newN) elements is preserved.
//
// This is the exact sequence std::vector performs internally when it grows.
// (notes 19.2: "Resizing means allocating a new array")
//
// Preconditions: p is a valid owning heap-array pointer; oldN >= 0; newN >= 0.
void resizeBuffer(int*& p, int oldN, int newN);
// ─── TASK 5 ──────────────────────────────────────────────────────────────────
// bufferSum(buf, n) — return the sum of all n elements. This is a PURE
// OBSERVER: it takes the pointer by value (copy of the address), the array is
// marked const, and nothing is allocated or freed.
//
// The const pointer makes it clear this function does not own or modify the
// array. In LLVM you see this constantly:
// int64_t sum(const int64_t* values, size_t count);
//
// Preconditions: buf != nullptr if n > 0; n >= 0.
int bufferSum(const int* buf, int n);
#endif // DYNBUF_H
# Chapter 19 — Dynamic Allocation · DynBuffer Workbench · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# Layout:
# dynbuf.h — DECLARATIONS only (provided, do not edit)
# starter/dynbuf.cpp — bodies with TASK blocks (learner fills in)
# solution/dynbuf.cpp — reference bodies (peek when stuck)
# tests/tests.cpp — grader: includes ../dynbuf.h, calls the API, CHECKs results
#
# The Makefile compiles tests/tests.cpp against EITHER starter or solution .cpp
# by swapping the second source file — same -I. brings dynbuf.h in by its bare name.
CXX := clang++
# -I. puts the chapter root on the include path so #include "dynbuf.h" resolves.
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── build: compile-check the learner's starter (warning-clean object file) ──
build:
$(CXX) $(CXXFLAGS) -c starter/dynbuf.cpp -o starter/dynbuf.o
@echo "OK \xE2\x9C\x85 starter/dynbuf.cpp compiles. Now run: make test"
# run: not meaningful for a pure library — remind the learner to use test.
run: build
@echo "(No standalone driver for this chapter. Use: make test)"
# ── test: grade the LEARNER's starter — RED until TASK blocks are filled ────
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/dynbuf.cpp -o tests/run
@./tests/run || echo "FAIL \xE2\x9D\x8C fill in the TASK blocks in starter/dynbuf.cpp until every check passes."
# ── solution: build + silently run the reference against its own tests ───────
solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/dynbuf.cpp -o tests/run_sol
@./tests/run_sol
# ── test-solution: proof the lab is solvable — MUST be green ─────────────────
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/dynbuf.cpp -o tests/run
@./tests/run
clean:
rm -f starter/dynbuf.o tests/run tests/run_sol
rm -rf tests/run.dSYM tests/run_sol.dSYM
make test locally
(see “Build & run locally” above).