Dynamic Memory Management

CS310 — new, delete, and Managing the Heap

Static memory is managed by the compiler. Dynamic memory is managed by you — allocate with new, free with delete.

Static vs Dynamic Memory

Why do we need dynamic memory at all?

Static — Compiler Manages

// Global: alive entire program
int globalCount = 0;
void func() {
// Local: alive during func() only
int scores[100]; // size must be fixed!
// What if we need 200? Or just 3?
}
The problem: Consider a grade roster. How many students? 30? 100? 500? With static allocation, you must pick a size at compile time — waste memory or risk overflow.

Dynamic — You Manage

void func() {
int n;
cout << "How many students? ";
cin >> n;
int *scores = new int[n]; // exact size!
// use scores[0] ... scores[n-1]
delete[] scores; // YOU must free it!
}
Dynamic memory lets you allocate exactly the amount you need, when you know how much that is (at runtime, not compile time).
Analogy: Static allocation is like buying a house — fixed size, decided upfront. Dynamic allocation is like renting — pick the size you need, but you must remember to cancel the lease (delete) when done.

The new Operator

Allocate memory on the heap at runtime

// Allocate a single int on the heap
int *p = new int;
*p = 42;
// Allocate an array on the heap
int *arr = new int[5];
arr[0] = 10; // use like normal array
// new returns a POINTER to the
// allocated memory on the heap
How new works: It asks the OS for memory from the heap, and returns the address (pointer) of that memory. The pointer itself lives on the stack, but the data lives on the heap.
If the heap is full, new throws a std::bad_alloc exception and the program terminates (unless you catch it).

Dynamic Memory Example

Allocate an array based on user input

Click Step to trace...

The delete Operator

Free heap memory when you're done with it

// Allocate
int *p = new int;
*p = 10;
// Free a single value
delete p;
// Allocate an array
int *arr = new int[100];
// Free an array — use delete[]!
delete[] arr;
// Good practice: set to nullptr after
p = nullptr;
arr = nullptr;
The rule: Every new must have a matching delete. Every new[] must have a matching delete[]. Miss one? Memory leak. Mix them up? Undefined behavior.
Only delete heap memory! Never delete a pointer to a stack variable — that's undefined behavior and will likely crash.
Analogy: new checks out a hotel room. delete checks out. If you forget to check out, you're still paying (memory leak). If you check out twice, chaos (double free).

The Memory Lifecycle

Allocate → Use → Free — step by step

Dangling Pointers

A pointer to memory that has been freed — the most dangerous bug

int *p = new int;
*p = 42;
delete p; // memory freed
// p still holds the old address!
cout << *p; // DANGLING! UB!
*p = 99; // Writing to freed memory!
// FIX: set to nullptr after delete
delete p;
p = nullptr; // now safe to check
Why it's dangerous: The freed memory might be reused for something else. Writing to it corrupts unrelated data. Reading it gives garbage. Both are undefined behavior — the program might seem to work, crash later, or produce wrong results.
Prevention: Always p = nullptr; right after delete p;. Then check if (p != nullptr) before using it.

Memory Leaks

Allocated memory that can never be freed — it's gone forever

void leaky() {
int *p = new int[1000];
// ... use the array ...
// OOPS! forgot delete[] p;
} // p destroyed, memory leaked!
void also_leaky() {
int *p = new int(5);
p = new int(10); // old memory leaked!
delete p; // only frees the second one
}
Two ways to leak:
  1. Forgetting to delete before the pointer goes out of scope
  2. Reassigning a pointer without freeing the old memory first
The golden rule: Every new must have exactly one matching delete. No more (double free), no less (leak). This is why modern C++ prefers smart pointers (unique_ptr, shared_ptr).
Analogy: A memory leak is like renting storage units and losing the keys. The units are still rented (memory still allocated), but you can never access or return them. Over time, you run out of units.

Returning Pointers from Functions

What you can and cannot return — and why

BAD: Return local array

int* reverse(const int* list, int size) {
int result[6]; // LOCAL (stack)
for (int i=0,j=size-1; i<size; i++,j--)
result[j] = list[i];
return result; // DANGLING!
}
Why it fails: result lives on the stack. When the function returns, that stack frame is destroyed. The pointer now points to freed memory — a dangling pointer!

GOOD: Return heap array

int* reverse(const int* list, int size) {
int* result = new int[size]; // HEAP
for (int i=0,j=size-1; i<size; i++,j--)
result[j] = list[i];
return result; // OK! Heap survives
}
Why it works: Heap memory persists until you explicitly delete[] it. The function allocates on the heap, returns the pointer, and the caller is responsible for freeing it.

Full Example: Reverse with Dynamic Memory

Allocate in function, use in caller, free when done

int* reverse(const int* list, int size) {
int* result = new int[size];
for (int i=0,j=size-1; i<size; i++,j--)
result[j] = list[i];
return result;
}
int main() {
int list[] = {1, 2, 3, 4, 5, 6};
int* p = reverse(list, 6);
// p = {6, 5, 4, 3, 2, 1}
printArray(p, 6);
delete[] p; // Don't forget!
return 0;
}
Ownership pattern: The function that calls new doesn't always call delete. Here, reverse() allocates but main() frees. The "owner" of the memory is whoever holds the pointer and is responsible for freeing it.

Stack vs Heap — When to Use Which

A decision guide for memory allocation

Challenge A — Spot the Memory Leak

Which code snippets have memory leaks?

Snippet 1

int *p = new int(5);
cout << *p;
delete p;

Snippet 2

int *p = new int(5);
p = new int(10);
delete p;

Snippet 3

void f() {
int *a = new int[10];
// use a...
}

Common Dynamic Memory Mistakes

Six pitfalls to avoid

1. Memory Leak — forgetting delete
void f() { int *p = new int; } // leaked!
2. Dangling Pointer — using after delete
delete p;
*p = 10; // undefined behavior!
3. Double Free — deleting twice
delete p;
delete p; // crash or corruption!
4. Mismatched new/delete
int *p = new int[10];
delete p; // should be delete[] p!
5. Deleting stack memory
int x = 5;
delete &x; // NOT from new! Crash!
6. Returning local pointer
int* f() { int x=5;
return &x; } // x destroyed on return!
Modern C++ solution: Use std::unique_ptr and std::shared_ptr — they automatically call delete when the pointer goes out of scope. Zero leaks, zero dangling pointers.

Challenge B — Fix the Bug

This function has a memory management error

Buggy Code

int* createArray(int size) {
int arr[size]; // VLA on stack
for (int i = 0; i < size; i++)
arr[i] = i * 10;
return arr; // returning local!
}
int main() {
int *p = createArray(5);
cout << p[2]; // undefined!
}

The Fix

Select and check to see the fix...

Preview: Smart Pointers (Modern C++)

The future: automatic memory management without garbage collection

Raw Pointer (Manual)

int *p = new int(42);
// ... use p ...
delete p; // YOUR responsibility
// What if you forget? → LEAK
// What if exception? → LEAK

Smart Pointer (Automatic)

#include <memory>
auto p = std::make_unique<int>(42);
// ... use *p ...
// automatically deleted when p goes
// out of scope! No leaks possible.

unique_ptr

Single owner. Deleted when owner goes out of scope. Can't be copied, only moved.

shared_ptr

Reference counted. Multiple owners. Deleted when last owner is destroyed.

weak_ptr

Non-owning observer. Doesn't prevent deletion. Used to break circular references.

Rule of thumb: In modern C++ (C++11 and later), prefer unique_ptr by default. Use shared_ptr only when you truly need shared ownership. Use raw new/delete only when you understand why you need it.

Dynamic Memory — Summary

The rules to live by

new T
alloc single
new T[n]
alloc array
delete p
free single
delete[] p
free array
p = nullptr
after delete
if (p)
check before use
unique_ptr
modern C++
make_unique
preferred
The Four Rules:
  1. Every new needs a delete
  2. Every new[] needs a delete[]
  3. Set pointer to nullptr after delete
  4. Never return pointers to local variables

Challenge C — Stack or Heap?

For each scenario, choose the right allocation strategy

1. A loop counter i used in a for loop

2. An array whose size is read from user input

3. Data that must outlive the function that creates it

Quiz — Multiple Choice

Q1: Where does new allocate?

Q2: What frees new int[10]?

Q3: A dangling pointer is:

Quiz — Trace Memory Operations

How many bytes are leaked?

int *a = new int[3]; // 12 bytes
int *b = new int[5]; // 20 bytes
int *c = a;
a = b;
delete[] a;
delete[] c;

Trace

Enter your answer, then click Check...

Quiz — Predict the Output

int* create(int val, int size) {
int* arr = new int[size];
for (int i = 0; i < size; i++)
arr[i] = val + i;
return arr;
}
int main() {
int* p = create(10, 4);
cout << p[0] << " "
<< p[2] << " "
<< *(p+3) << endl;
delete[] p;
}

Trace

Enter your answer, then click Check.