Advanced • Memory
Memory Allocation Internals
By the end of this lesson you'll understand exactly what happens when you write new, how it differs from malloc, how to build objects in memory you already own with placement new, how to hook operator new and use std::allocator, and why allocation is expensive enough that pools and arenas exist.
What You'll Learn
- Trace the chain new → operator new → malloc → the OS
- Choose between new/delete and malloc/free, and never mix them
- Use placement new to construct objects in memory you own
- Override operator new / operator delete to control allocation
- Use std::allocator and see how STL containers allocate
- Explain heap fragmentation and why allocation is slow — then beat it with a pool
💡 Real-World Analogy
Think of the heap as a warehouse. operator new is the warehouse clerk: when you ask for space, they walk the shelves looking for a free spot big enough, hand you the key, and log it. malloc is the loading dock that the clerk uses to bring in a whole pallet from outside (the OS) when the shelves run low. Constructing the object is unpacking your goods onto that shelf; placement new is unpacking onto a shelf you already rented. After lots of random pick-ups and returns the shelves get pockmarked with little gaps — that's fragmentation: lots of total space, but no single run long enough for a big crate. A pool allocator is renting one aisle of identical, interchangeable boxes so every request is instant and nothing ever fragments.
📊 The Allocation Chain
| Layer | Who calls it | Job |
|---|---|---|
| new T(...) | You | Allocate bytes, then run the constructor |
| operator new | The compiler | Return raw, uninitialised bytes (or throw) |
| malloc | operator new | Carve a slice from the heap's free lists |
| mmap / sbrk | malloc | Ask the OS for more heap when needed |
Each layer adds work. The deeper a single new has to fall down this chain — especially into a system call — the more expensive it is. Most of allocator design is about staying near the top.
1. How new and delete Really Work
When you write int* p = new int(42); two distinct steps happen. First operator new(sizeof(int)) hands back raw, uninitialised bytes. Then the constructor runs in those bytes to make a real object. delete reverses it: it runs the destructor first, then operator delete returns the bytes. Arrays use the bracket forms — new[] and delete[] — and they must match, because new[] secretly records the element count so delete[] knows how many destructors to run.
Pro Tip: new(std::nothrow) T returns nullptr on failure instead of throwing std::bad_alloc — handy in embedded or no-exceptions code where you'd rather check a pointer than catch.
Worked example: new / delete mechanics
Single objects, arrays, and nothrow allocation — read every comment.
#include <iostream>
using namespace std;
int main() {
// new does TWO things: grabs raw bytes, then runs the constructor.
int* p = new int(42); // allocate one int on the heap, set to 42
cout << "Value: " << *p << endl; // Value: 42 (*p reads through the pointer)
delete p; // free it: bytes returned to the allocator
p = nullptr; // good habit: avoid a dangling pointer
// Arrays use new[] / delete[] -> they MUST match
...2. new/delete vs malloc/free
malloc and free come from C. They only move bytes — they never run a constructor or destructor, so a malloc'd object is full of garbage until you build it yourself. new and delete are the C++ pair: they allocate and construct, free and destruct. The rule is absolute: memory from new is freed with delete, memory from malloc is freed with free. Crossing the streams is undefined behaviour.
Worked example: malloc bytes vs new objects
See why malloc alone leaves an object unbuilt, and how new does both jobs.
#include <iostream>
#include <cstdlib> // malloc, free
#include <new> // placement new
using namespace std;
struct Point {
int x, y;
Point(int a, int b) : x(a), y(b) { // constructor
cout << "Point(" << x << "," << y << ") built" << endl;
}
~Point() { cout << "Point destroyed" << endl; }
};
int main() {
// malloc/free are C: they move BYTES and know nothing about objects.
Point* a = (Point*) malloc(sizeof(Point)); // raw bytes only -> NO construct
...Your turn. Fill in the three blanks below to allocate and free both a single value and an array — and remember the bracket rule for arrays.
🎯 Your turn: allocate and free correctly
Fill in the ___ blanks, then check your output against the expected lines.
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// 1) Allocate ONE double on the heap, initialised to 3.14
double* pi = ___; // 👉 new double(3.14)
cout << "pi = " << *pi << endl;
// 2) Free that single double (single object -> plain delete)
___; // 👉 delete pi;
// 3) Allocate an array of 4 ints, then free it with the matching delete
int* scores = new int[4]
...3. Placement New — Construct Without Allocating
Placement new splits the two steps apart. You give it an address you already own — new(ptr) Type(args) — and it just runs the constructor there, allocating nothing. This is the engine inside std::vector (which builds elements in its own buffer) and every custom allocator. The catch: because you own the memory, you are responsible for calling the destructor by hand (obj->~Type();) — never delete, which would also try to free memory you didn't get from new.
Common Mistake: forgetting alignas on your buffer. If the bytes aren't aligned for the type, placement new gives you a misaligned object — undefined behaviour and a crash on many CPUs.
Worked example: placement new in a stack buffer
Construct two objects in raw bytes, then destruct them by hand.
#include <iostream>
#include <new> // for placement new
using namespace std;
struct Sensor {
int id;
double reading;
Sensor(int i, double r) : id(i), reading(r) {
cout << "Sensor " << id << " constructed" << endl;
}
~Sensor() { cout << "Sensor " << id << " destroyed" << endl; }
};
int main() {
// A raw byte buffer big enough for 2 Sensors, correctly aligned.
alignas(Sensor) char buffer[sizeof(Sensor) * 2];
// Placement new: build the object AT an addr
...Now you try. A correctly-aligned buffer is already set up — construct a Widget in it with placement new, then destroy it yourself:
🎯 Your turn: construct in a buffer you own
Use new(buffer) Widget(7) and call the destructor by hand.
#include <iostream>
#include <new>
using namespace std;
struct Widget {
int id;
Widget(int i) : id(i) { cout << "Widget " << id << " built" << endl; }
~Widget() { cout << "Widget " << id << " gone" << endl; }
};
int main() {
// 🎯 YOUR TURN — a buffer is ready; construct and destroy in it.
alignas(Widget) char buffer[sizeof(Widget)];
// 1) Construct a Widget with id 7 INSIDE buffer using placement new
Widget* w = ___; // 👉 new(buffer) Widget(7)
cout
...4. Custom operator new / operator delete
You can replace the global operator new and operator delete with your own. The compiler keeps generating new T the same way, but the raw-byte step now runs your code — useful for logging every allocation, adding guard bytes to catch overruns, or routing to a custom heap. Your operator new must return a valid pointer or throw std::bad_alloc; your operator delete must be noexcept. You can also overload them per class for fine-grained control.
Worked example: log every allocation
Override global operator new/delete to print each alloc and free.
#include <iostream>
#include <cstdlib> // malloc, free
using namespace std;
// Override the GLOBAL operator new / delete to log every allocation.
// new(size) calls this; delete(ptr) calls operator delete below.
void* operator new(size_t size) {
cout << " [alloc " << size << " bytes]" << endl;
void* p = malloc(size); // get the raw bytes
if (!p) throw bad_alloc(); // contract: throw on failure
return p;
}
void operator delete(void* p) noexcept {
cout << "
...5. std::allocator — the Standard Interface (Brief)
Every STL container takes an allocator — a small object that says how to get and release memory. The default is std::allocator<T>, and it formalises exactly the split you've just seen: allocate(n) returns raw space for n objects, you construct each one in place (placement new under the hood), then destroy and deallocate in mirror order. Swapping in your own allocator type is how you make a container use a pool or arena.
Worked example: std::allocator step by step
Allocate raw space, construct, use, destroy, deallocate — the STL pattern.
#include <iostream>
#include <memory> // std::allocator
using namespace std;
int main() {
// std::allocator is the policy every STL container uses underneath.
// It splits allocation (raw bytes) from construction (run constructor),
// exactly like operator new + placement new, but as a reusable object.
allocator<int> alloc;
// 1) allocate raw, uninitialised space for 3 ints
int* data = alloc.allocate(3);
// 2) construct each value in place (this is placement new
...🔎 Deep Dive: Why Allocation Is Expensive (and Fragmentation)
A single new looks cheap but isn't. The allocator has to search its free lists or size-class bins for a fitting block, often take a lock so two threads don't corrupt the heap, sometimes make a system call (mmap/sbrk) to grow the heap, and the memory it returns is frequently a cache miss. In a loop allocating millions of small objects, that overhead — not your actual work — becomes the bottleneck.
Fragmentation makes it worse over time. Allocate and free blocks of many different sizes and the free space splinters into scattered gaps. You can hold megabytes of free memory yet fail a single large request because no one gap is big enough.
Heap after mixed alloc/free (each cell = a chunk):
[USED][free][USED][USED][free][USED][free]
^4KB ^4KB ^4KB -> 12KB free total,
but no 8KB run!Fixed-size pool allocators dodge both problems: every slot is identical and interchangeable, so allocation is just popping a free list (O(1), no search, no syscall) and freed slots fit any future request, so there's nothing to fragment.
6. Beating the Cost: a Pool Allocator
A pool pre-allocates a block of fixed-size slots once, then hands them out by popping a free list and reclaims them by pushing back — both O(1), with no system calls and zero fragmentation. Pools shine when you create and destroy many objects of the same type: particle systems, network packets, game entities. Read this from-scratch pool and watch a freed slot get reused.
Worked example: a free-list pool allocator
O(1) allocate/deallocate, and a freed slot reused on the next request.
#include <iostream>
using namespace std;
// A pool pre-allocates fixed-size slots. allocate()/deallocate() just
// pop/push a free list: O(1), no system calls, no fragmentation.
template<typename T, size_t N = 8>
class Pool {
union Slot { T value; Slot* next; }; // a slot is either a value OR a link
Slot slots[N];
Slot* freeList;
public:
Pool() {
freeList = &slots[0]; // chain every slot together
for (size_t i = 0; i < N - 1; i++) slots[i].
...Pro Tips
- 💡 Reach for RAII first:
std::make_unique,std::make_shared, andstd::vectorfree memory automatically — rawnew/deleteis the exception, not the rule. - 💡 Match every allocation:
new↔delete,new[]↔delete[],malloc↔free, placement new↔manual destructor. Mixing them is undefined behaviour. - 💡 Profile before optimising: only write a custom allocator once a profiler proves allocation is the bottleneck. The default allocator is fast and correct.
- 💡 Null out after delete: set the pointer to
nullptrso a stray later use fails loudly instead of corrupting freed memory.
Common Errors (and the fix)
- Mismatched new[]/delete: calling
delete p;on memory fromnew int[5]is undefined behaviour — the element-count metadata is ignored. Use the matchingdelete[] p;. - Double free / use-after-free:
delete p;twice, or using*pafterdelete, corrupts the heap. Delete once, then setp = nullptr;. - Calling free on new'd memory:
int* p = new int; free(p);mixes the C and C++ heaps. Freenewwithdelete, andmallocwithfree— never cross them. - Placement new then delete: writing
delete obj;afternew(buffer) T()tries to free memoryoperator newnever gave out. Callobj->~T();instead and free the buffer yourself. - Missing alignas: placement new into a plain
char buf[N]may be misaligned for the type. Declare the bufferalignas(T) char buf[sizeof(T)];. - Forgetting the destructor: with placement new the destructor never runs on its own — skip it and you leak whatever the object held (file handles, inner allocations). Always call
~T()by hand.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Allocate one object | new T(args) | alloc + construct |
| Free one object | delete p; | destruct + free |
| Allocate array | new T[n] | use delete[] |
| No-throw alloc | new(nothrow) T | nullptr on fail |
| Raw C alloc | malloc(size) | bytes only; free |
| Construct in place | new(ptr) T(args) | manual ~T() |
| STL allocator | std::allocator<T> | allocate/construct |
Frequently Asked Questions
Q: Should I use new/delete or malloc/free in modern C++?
Neither, most of the time. Prefer std::make_unique and std::make_shared, or containers like std::vector, which free memory for you. When you do need raw allocation, use new/delete in C++ because they run constructors and destructors; malloc/free only move bytes and know nothing about C++ objects.
Q: What actually happens when I write 'new'?
Two steps. First operator new(size) grabs raw bytes — it usually forwards to malloc, which asks the OS for a big chunk via mmap or sbrk and hands you a slice. Then the constructor runs in those bytes to build the object. delete reverses it: destructor first, then operator delete frees the bytes.
Q: Why is allocation considered slow?
A single new is not one machine instruction — it walks the allocator's free lists or size-class bins, may take a lock so threads do not corrupt each other, can trigger a system call to grow the heap, and the returned memory is often a cache miss. In a hot loop that overhead dominates, which is why pools and arenas exist.
Q: What is placement new for?
Placement new constructs an object in memory you already own instead of allocating new memory. You pass it an address — new(ptr) Type(args) — and it just runs the constructor there. It is how std::vector builds elements inside its buffer and how every custom allocator turns raw bytes into live objects.
Q: What is heap fragmentation?
After many allocations and frees of different sizes, the free memory ends up split into small scattered gaps. You can have plenty of total free space yet still fail to allocate one large block because no single gap is big enough. Fixed-size pool allocators avoid this because every slot is identical and interchangeable.
Q: When should I write a custom allocator?
Only after profiling shows allocation is a real bottleneck — game entities, particle systems, network packets, or any tight loop creating and destroying many same-size objects. For everyday code the default allocator is fast and correct; a custom one adds risk you should not pay for without evidence.
Mini-Challenge: Manual Dynamic Array
No blanks this time — just a brief and an outline. Allocate an array yourself, fill it, print it, and free it with the matching delete[]. Check your output against the example in the comments.
🎯 Mini-Challenge: build, fill, print, free
Allocate new int[5], store squares, print them, then delete[] it.
#include <iostream>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: manual dynamic array
// 1. Ask the allocator for an array of 5 ints with new int[5]
// 2. Fill it in a loop so element i holds i * i (0, 1, 4, 9, 16)
// 3. Print all five values on one line, separated by spaces
// 4. Free it with the MATCHING delete[] (brackets, because it's an array)
//
// ✅ Expected output:
// 0 1 4 9 16
// your code here
return 0;
}🎉 Lesson Complete
- ✅
new=operator new(raw bytes) + constructor;delete= destructor +operator delete - ✅
new/deleterun constructors;malloc/freeonly move bytes — never mix the pairs - ✅ Placement new
new(ptr) T(args)builds in memory you own; you call~T()by hand - ✅ You can override
operator new/operator deleteto log, guard, or reroute allocation - ✅
std::allocatorformalises allocate → construct → destroy → deallocate for STL containers - ✅ Allocation is costly (search, locks, syscalls, cache misses) and fragments — pools fix both
- ✅ Next lesson: Files & Streams — read and write data on disk
Sign up for free to track which lessons you've completed and get learning reminders.