Memory Allocation Internals

Reviewed & published by Brayan K

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.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 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

LayerWho calls itJob
new T(...)YouAllocate bytes, then run the constructor
operator newThe compilerReturn raw, uninitialised bytes (or throw)
mallocoperator newCarve a slice from the heap's free lists
mmap / sbrkmallocAsk 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.

#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.
    int* arr = new int[3]{10, 20, 30}; // allocate 3 ints in a row
    for (int i = 0; i < 3; i++)
        cout << arr[i] << " ";          // 10 20 30
    cout << endl;
    delete[] arr;                   // delete[] (with brackets!) frees an array

    // new(nothrow): on failure you get nullptr instead of an exception.
    int* big = new(nothrow) int[100];
    if (!big) cout << "Allocation failed" << endl;
    else { cout << "Got the memory" << endl; delete[] big; }

    return 0;
}

// ✅ Expected output:
//    Value: 42
//    10 20 30
//    Got the memory

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.

#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 constructor ran
    // a->x is garbage here because the constructor never ran!
    new (a) Point(1, 2);   // placement new runs the constructor in those bytes
    cout << "a = (" << a->x << "," << a->y << ")" << endl; // a = (1,2)
    a->~Point();           // call destructor by hand (malloc won't do it)
    free(a);               // then release the raw bytes

    // new/delete are C++: they run constructor AND destructor for you.
    Point* b = new Point(3, 4);   // allocate + construct in one step
    delete b;                     // destruct + free in one step

    return 0;
}

// ✅ Expected output:
//    Point(1,2) built
//    a = (1,2)
//    Point destroyed
//    Point(3,4) built
//    Point destroyed

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.

#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]{90, 80, 70, 60};
    cout << "first = " << scores[0] << endl;
    ___;                       // 👉 delete[] scores;   (brackets for arrays!)

    // ✅ Expected output:
    //    pi = 3.14
    //    first = 90
    return 0;
}

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.

#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 address you already own.
    Sensor* s1 = new(buffer) Sensor(1, 23.5);                 // first slot
    Sensor* s2 = new(buffer + sizeof(Sensor)) Sensor(2, 18.9); // second slot

    cout << "s1 reading: " << s1->reading << endl;  // s1 reading: 23.5
    cout << "s2 reading: " << s2->reading << endl;  // s2 reading: 18.9

    // No delete here — the buffer is on the stack. But you DO owe each
    // object a destructor call, in reverse order:
    s2->~Sensor();   // Sensor 2 destroyed
    s1->~Sensor();   // Sensor 1 destroyed
    return 0;
}

// ✅ Expected output:
//    Sensor 1 constructed
//    Sensor 2 constructed
//    s1 reading: 23.5
//    s2 reading: 18.9
//    Sensor 2 destroyed
//    Sensor 1 destroyed

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

#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 << "id = " << w->id << endl;

    // 2) You own the buffer, so call the destructor BY HAND (no delete!)
    ___;                      // 👉 w->~Widget();

    // ✅ Expected output:
    //    Widget 7 built
    //    id = 7
    //    Widget 7 gone
    return 0;
}

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.

#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 << "  [free]" << endl;
    free(p);
}

int main() {
    cout << "Make an int:" << endl;
    int* a = new int(5);   //   [alloc 4 bytes]   then constructor runs
    cout << "*a = " << *a << endl;
    delete a;              //   [free]

    cout << "Make a double:" << endl;
    double* d = new double(1.5); //   [alloc 8 bytes]
    delete d;                    //   [free]
    return 0;
}

// ✅ Expected output:
//    Make an int:
//      [alloc 4 bytes]
//    *a = 5
//      [free]
//    Make a double:
//      [alloc 8 bytes]
//      [free]

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.

#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 under the hood)
    for (int i = 0; i < 3; i++)
        construct_at(&data[i], (i + 1) * 100);   // 100, 200, 300

    for (int i = 0; i < 3; i++)
        cout << data[i] << " ";                  // 100 200 300
    cout << endl;

    // 3) destroy each object, then 4) free the raw bytes — mirror order.
    for (int i = 0; i < 3; i++) destroy_at(&data[i]);
    alloc.deallocate(data, 3);
    return 0;
}

// ✅ Expected output:
//    100 200 300

🔎 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.

#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].next = &slots[i + 1];
        slots[N - 1].next = nullptr;
    }
    T* allocate() {                                 // O(1): pop the free list
        if (!freeList) throw bad_alloc();
        Slot* s = freeList;
        freeList = freeList->next;
        return &s->value;
    }
    void deallocate(T* p) {                          // O(1): push back on the list
        Slot* s = reinterpret_cast<Slot*>(p);
        s->next = freeList;
        freeList = s;
    }
};

int main() {
    Pool<int> pool;
    int* a = pool.allocate(); *a = 100;
    int* b = pool.allocate(); *b = 200;
    cout << "a=" << *a << " b=" << *b << endl;   // a=100 b=200

    pool.deallocate(a);                           // a's slot goes back
    int* c = pool.allocate(); *c = 300;           // c REUSES a's slot
    cout << "c=" << *c << " (reused slot)" << endl; // c=300 (reused slot)

    pool.deallocate(b);
    pool.deallocate(c);
    return 0;
}

// ✅ Expected output:
//    a=100 b=200
//    c=300 (reused slot)

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Allocate one objectnew T(args)alloc + construct
Free one objectdelete p;destruct + free
Allocate arraynew T[n]use delete[]
No-throw allocnew(nothrow) Tnullptr on fail
Raw C allocmalloc(size)bytes only; free
Construct in placenew(ptr) T(args)manual ~T()
STL allocatorstd::allocator<T>allocate/construct

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.

#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

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();
  • delete[] p;

Answer: delete[] p;. 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.

Continue this course

Frequently asked questions

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.

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.

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.

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.

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.

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.

Related lessons