Dynamic Memory
Asking the operating system for memory at runtime with new and delete — and why we want to avoid doing it by hand.
So far every variable we created lived on the stack: created at function entry, destroyed at function exit, sized at compile time. That is fast and safe, but it is not enough for every program.
What if you don't know how big something will be until the program is running? What if an object needs to outlive the function that created it? You need a different region of memory whose lifetime you control: the heap.
Stack vs. heap, one more time
Asking for heap memory used to be done with raw new and delete.
Modern C++ has much better tools (we'll meet them in the smart
pointer chapter). But you need to understand the raw mechanism
first, because every higher-level tool is built on top of it.
new and delete
The new expression does two things:
- Asks the allocator for enough bytes to hold one object of the given type.
- Constructs an object in those bytes and returns a pointer to it.
delete undoes both steps: it runs the object's destructor and
returns the bytes to the allocator.
A picture of what happened:
After delete p, the heap slot is gone but p still holds the old
address — it has become a dangling pointer. That is why we set
p = nullptr; immediately after.
Arrays on the heap
To allocate n objects in a row, use new[] and pair it with
delete[]:
The mismatch trap.
newmust be matched withdelete, andnew[]withdelete[]. Mixing them is undefined behavior. The easiest way to never get this wrong is to not writenew/deleteat all and let containers and smart pointers do it for you.
What can go wrong
There are essentially three categories of bug, and you must learn to recognize all of them.
1. Memory leak
You allocated, but never freed. The OS will reclaim it when the process ends, but a long-running program will slowly eat all available memory.
void leaker() {
int* p = new int(1);
// ... forgot delete p ...
} // p is gone; the heap object is now unreachable but still allocated2. Use-after-free
You used the pointer after deleting it.
int* p = new int(1);
delete p;
std::cout << *p; // UNDEFINED BEHAVIOR — anything can happen3. Double-delete
You called delete on the same pointer twice.
int* p = new int(1);
delete p;
delete p; // UNDEFINED BEHAVIORHeap corruption is particularly nasty because the crash often happens somewhere else, much later, in code that has nothing to do with your bug.
Why this is the wrong style for everyday code
Look at how much energy these examples spend on bookkeeping. Every
new must be balanced by exactly one delete, on every code path,
including exception paths. That is fragile.
Modern C++ flips the model around: instead of writing new and
delete by hand, you let objects own their resources, and the
language calls the destructor automatically when those objects go
out of scope. This is called RAII (Resource Acquisition Is
Initialization), and it's the single most important idea in C++.
Two everyday examples of RAII you've already seen without realizing it:
std::vector<int>callsnew[]/delete[]for you. You writevec.push_back(x), the vector takes care of the heap.std::stringdoes the same for character storage.
A preview of the safer version of dynamic memory, using
std::unique_ptr:
We'll devote a whole chapter to smart pointers. For now, just know
that raw new/delete are advanced tools you should rarely need
in modern application code. When you do need them — implementing
your own container, or interfacing with a C library — you'll know
why.
Sizing intuition
You can ask the language how big a type is. This is useful when estimating memory usage.
The exact numbers depend on the target. On the WebAssembly target
used by this course's runtime, int* is 4 bytes; on a typical
64-bit desktop it would be 8.
Challenges
Allocate a single int on the heap with the value 123. Print it. Then free it. The output should be exactly 123. Make sure you do not leak memory.
Read an integer n (assume 5 for testing). Allocate a heap array of n ints, fill it with the squares of 1..n (so for n = 5 you get 1 4 9 16 25), print the numbers space-separated, then delete[] the array. For the test, main is already wired with n = 5 and expects 1 4 9 16 25 followed by a newline.
Test your understanding
What does the expression new T(args...) do?
Reserves a slot on the stack.
Returns a copy of T.
Allocates enough memory for one T on the heap, constructs a T in it, and returns a pointer to it.
Frees existing memory and reallocates it.
Why must new[] be paired with delete[] (not plain delete)?
The compiler will refuse to compile mismatched calls.
The OS tracks them separately on disk.
new[] may record an element count alongside the allocation, and delete[] knows how to invoke each destructor and release the whole block; delete does not.
They are interchangeable in modern C++.
What is the primary danger of a memory leak?
The program will crash immediately.
It causes data corruption in unrelated objects.
Memory is allocated but never freed, so a long-running program can exhaust available memory over time.
It triggers a compile-time error.
Which of the following is the modern way to manage a single heap-allocated object?
Call new and remember to delete it on every code path.
Use malloc/free.
Use std::unique_ptr (often via std::make_unique); its destructor frees the object automatically.
Allocate it on the stack with new followed by no delete.
Next: aggregates and strings — how to bundle data, and the tools the standard library gives you so you almost never need raw arrays again.