Memory Allocation

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.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

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.

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.

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.

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.

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.

Now you try. A correctly-aligned buffer is already set up — construct a Widget in it with placement new, then destroy it yourself:

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.

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.

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.

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.

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.

Practice quiz

What two steps does 'new T(args)' perform?

  • Only allocates raw bytes
  • Only runs the constructor
  • Allocates raw bytes (operator new), then runs the constructor in them
  • Calls free()

Answer: Allocates raw bytes (operator new), then runs the constructor in them. new does two things: operator new grabs raw bytes, then the constructor runs in those bytes. delete reverses it: destructor, then operator delete.

How do new/delete differ from malloc/free?

  • new/delete run constructors and destructors; malloc/free only move bytes
  • They are identical
  • malloc runs constructors
  • delete is faster than free always

Answer: new/delete run constructors and destructors; malloc/free only move bytes. malloc/free are C — they only move bytes and know nothing about objects. new/delete allocate AND construct, free AND destruct.

Which delete form must you use for memory from 'new int[5]'?

  • delete p;
  • free(p);
  • p->~int();

Array allocations from new[] must be freed with delete[]. new[] records the element count so delete[] knows how many destructors to run.

What does placement new (new(ptr) Type(args)) do?

  • Allocates new memory and constructs
  • Runs the constructor in memory you already own, allocating nothing
  • Frees memory
  • Copies an object

Answer: Runs the constructor in memory you already own, allocating nothing. Placement new constructs an object at an address you already own. It allocates nothing — it is how std::vector builds elements in its own buffer.

After using placement new, how do you destroy the object?

  • Call the destructor by hand: obj->~Type();
  • delete obj;
  • free(obj);
  • Nothing — it is automatic

Answer: Call the destructor by hand: obj->~Type();. You own the memory, so you call the destructor explicitly (obj->~Type();). Using delete would try to free memory operator new never handed out.

What does 'new(nothrow) T' do on allocation failure?

  • Throws std::bad_alloc
  • Crashes
  • Returns nullptr instead of throwing
  • Retries forever

Answer: Returns nullptr instead of throwing. new(nothrow) returns nullptr on failure instead of throwing std::bad_alloc — handy in embedded or no-exceptions code where you check a pointer.

What must your overridden global operator new do on failure?

  • Return nullptr
  • Return a valid pointer or throw std::bad_alloc
  • Call exit()
  • Return 0 bytes

Answer: Return a valid pointer or throw std::bad_alloc. The contract: operator new returns a valid pointer or throws std::bad_alloc. The matching operator delete must be noexcept.

What is heap fragmentation?

  • Running out of total memory
  • A type of cache miss
  • When malloc is called twice
  • After many mixed allocs/frees, free space splits into scattered gaps, so a large request can fail despite plenty of free total

Answer: After many mixed allocs/frees, free space splits into scattered gaps, so a large request can fail despite plenty of free total. Fragmentation splinters free memory into small gaps. You can hold megabytes free yet fail one large request because no single gap is big enough.

Why is a fixed-size pool allocator fast and fragmentation-free?

  • It uses the OS directly each time
  • allocate/deallocate just pop/push a free list (O(1)), and every identical slot fits any future request
  • It never frees memory
  • It compresses memory

Answer: allocate/deallocate just pop/push a free list (O(1)), and every identical slot fits any future request. A pool pre-allocates identical slots. Allocation pops the free list and deallocation pushes back — both O(1), no syscalls, and interchangeable slots can't fragment.

What does std::allocator separate, mirroring operator new + placement new?

  • Reading and writing
  • Stack from heap
  • Allocation (raw bytes) from construction (running the constructor)
  • Pointers from references

Answer: Allocation (raw bytes) from construction (running the constructor). std::allocator splits allocate(n) (raw space) from constructing each object in place, then destroy and deallocate in mirror order — the STL container pattern.