Skip to main content

    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

    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.

    Worked example: new / delete mechanics

    Single objects, arrays, and nothrow allocation — read every comment.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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, and std::vector free memory automatically — raw new/delete is the exception, not the rule.
    • 💡 Match every allocation: newdelete, new[]delete[], mallocfree, 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 nullptr so a stray later use fails loudly instead of corrupting freed memory.

    Common Errors (and the fix)

    • Mismatched new[]/delete: calling delete p; on memory from new int[5] is undefined behaviour — the element-count metadata is ignored. Use the matching delete[] p;.
    • Double free / use-after-free: delete p; twice, or using *p after delete, corrupts the heap. Delete once, then set p = nullptr;.
    • Calling free on new'd memory: int* p = new int; free(p); mixes the C and C++ heaps. Free new with delete, and malloc with free — never cross them.
    • Placement new then delete: writing delete obj; after new(buffer) T() tries to free memory operator new never gave out. Call obj->~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 buffer alignas(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

    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

    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.

    Try it Yourself »
    C++
    #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/delete run constructors; malloc/free only 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 delete to log, guard, or reroute allocation
    • std::allocator formalises 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.