2Brute2Best
๐Ÿ”ฅ0
M1.3

Stack vs Heap, Placement new & Memory Layout

Medium High-frequency interview topicStack FrameHeap Allocatornew[] HeaderPlacement newExplicit ~T()
โšก 15-Second Mental ModelM1.3 ยท Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Stack vs Heap)

The stack is a notepad on your desk (scribble notes and tear them off in 1 second; zero cleanup needed). The heap is a rented warehouse across town (infinite space, but requires an Uber ride to visit and you must remember to cancel the lease).

Rule to Remember: Stack moves in 1 CPU instruction (sub rsp); Heap requires OS allocator search.
3 Golden Facts to Remember
Fact #1
Why is stack allocation O(1) and deterministic, while heap allocation is non-deterministic?

Stack allocation is a single CPU instruction (sub rsp, N) that decrements the stack pointer register. It has zero branching, requires no synchronization locks, and never fragments memory. Heap allocation, however, must query user-space allocator bins, search free-lists (best-fit/first-fit), acquire arena mutexes in multithreaded environments, and potentially invoke OS kernel syscalls (brk/mmap) to map new virtual memory pages. This introduces latency variance and potential CPU cache misses.

Fact #2
What exact sequence of operations occurs when you execute `Widget* w = new Widget(42);`?

The expression new Widget(42) is a C++ language expression that executes two distinct steps:

  1. Memory Allocation: The compiler calls the allocation function void* mem = ::operator new(sizeof(Widget)). If allocation fails, it invokes the std::new_handler or throws std::bad_alloc.
  2. Object Construction: The compiler runs the constructor Widget::Widget(42) in-place at address mem. If the constructor throws an exception, the runtime automatically calls ::operator delete(mem) to prevent a memory leak before propagating the exception.
Fact #3
How does delete[] know how many elements to destruct, and why does mixing new[] with scalar delete cause crashes?

For types with non-trivial destructors, new T[N] allocates extra storage (typically 8 bytes on 64-bit platforms) immediately before the first array element to store N, known as the array cookie. The returned pointer points to element 0 (raw_ptr + 8). delete[] ptr reads ptr[-1] to determine the element count, calls destructors from N-1 down to 0, and frees memory starting at ptr - 8. Calling scalar delete ptr skips the cookie, calls only ptr[0]'s destructor, and passes ptr instead of ptr - 8 to the heap free-list, corrupting allocator chunk headers.

Pinned Concepts in this Chapter (Click to Locate in Text):
View All 1 Chapter Q&As
01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

Interviewers probe stack versus heap allocation to see whether you view memory as an abstract infinite sheet or as physical CPU registers, cache lines, and operating system pages:

  • The Amateur View: Simply repeating 'the stack is fast and automatic, while the heap is dynamic and slow.'
  • The Senior View: Explaining that the stack is an ultra-fast, local scratchpad managed by a single hardware register, whereas the heap is a shared warehouse governed by dynamic freelists and locks.
  • What Evaluators Test:
    1. Instruction-Level Mechanics: Why allocating stack memory is a single CPU subtraction (sub rsp, N) while heap allocation navigates lock contention, freelist bins, and OS kernel page requests (brk/mmap).
    2. The Array Cookie Contract: How delete[] reads the hidden 8-byte prefix cookie (ptr[-1]) to determine destructor iterations, and why mixing scalar delete corrupts allocator headers.
    3. Decoupled Lifecycle: How placement new separates raw memory allocation from object construction, empowering low-latency memory pools and STL containers.
02

Under the hood & memory layout

Hardware & memory representation

Picture a process's virtual memory as a skyscraper with a top-floor penthouse and a ground-floor loading dock:

  • The Stack grows downward from the high-address penthouse: fast, private, and managed by a single sliding marker (RSP).
  • The Heap expands upward from the low-address ground floor: flexible, shared across all threads, but requiring a warehouse manager to find free space.

Under the hood:

  1. The Stack Bump: Local variables take space by sliding the CPU stack pointer down (sub rsp, 32). Deallocation is just sliding it back up (add rsp, 32). It costs a fraction of a nanosecond and stays primed in the L1 CPU cache.
  2. The Heap Dance: Standard new T performs two distinct operations: it calls ::operator new to search allocator free-lists for a suitable chunk, then calls the constructor T::T(...) to initialize the object.
  3. Placement New Decoupling: Placement new bypasses the allocator entirely. It runs a constructor directly inside a pre-existing memory buffer (new (buffer) T(...)), giving systems engineers total control over layout and performance.
Process Memory Map (Low to High)

.text (executable code) โ†’ .rodata (read-only data) โ†’ .data (initialized statics) โ†’ .bss (zeroed statics) โ†’ Heap โ†‘ (managed by glibc ptmalloc / jemalloc) โ†’ Memory Mapping Area (mmap shared libs) โ†’ Stack โ†“ (starts near 0x7fffffffffff).

Stack Frame (RSP Pointer Bump)

Allocating local variables is a single CPU instruction: sub rsp, N. Freeing them at scope exit is add rsp, N. Hot cache lines mean stack variables almost always reside in CPU L1/L2 data cache (~1 ns access latency).

Heap Allocation Chunk Header

Heap allocators prefix every allocated block with an 8-to-16 byte chunk header storing chunk size and allocation flags (e.g. PREV_INUSE). Allocating 4 bytes often consumes 24 to 32 bytes of physical RAM.

Array Cookie in new T[N]

When T has a non-trivial destructor, new T[N] secretly allocates sizeof(size_t) + N * sizeof(T) bytes. The compiler stores N in an 8-byte cookie right before the returned pointer. delete[] reads ((size_t*)ptr)[-1] to run exactly N destructors.

Placement new & Alignment

Placement new (new (addr) T(...)) constructs an object inside pre-existing memory. The buffer must meet alignof(T). Explicit destructor ptr->~T() must be called manually; calling delete ptr passes non-heap memory to the free-list, corrupting memory.

03

Core concepts & syntax traps

01

Process Memory Segments (.text, .rodata, .data, .bss, Heap, Stack)

When the operating system launches your executable, it carves the virtual address space into dedicated functional zones:

  1. Static Binary Segments (Low Addresses):
    • .text (Code): Read-only machine instructions executed directly by the CPU core.
    • .rodata (Constants): String literals and virtual tables (vtables), strictly write-protected.
    • .data (Initialized Globals): Non-zero global and static variables backed by bytes stored in the executable file.
    • .bss (Zeroed Globals): Uninitialized or zero-valued statics. Takes 0 bytes on disk; the OS kernel supplies zero-filled pages on demand.
  2. Dynamic Runtime Segments (High/Mid Addresses):
    • The Heap (Grows Upward): Shared pool managed by runtime allocators (ptmalloc, jemalloc) via kernel requests (brk/mmap).
    • The Stack (Grows Downward): Thread-local execution frames pushed and popped via the CPU's stack pointer register (RSP).
C++23 snippet
int g_init = 100;         // .data segment
int g_uninit;             // .bss segment (zeroed)
const char* msg = "Test"; // pointer in .data, string literal in .rodata

void inspect() {
    int stackVar = 5;     // Stack segment (high address)
    int* heapVar = new int(10); // Heap segment (low/mid address)
    delete heapVar;
}
Interview takeaway:

Globals and statics live in .data/.bss for the program lifetime; local variables live on the Stack; dynamically allocated memory lives on the Heap.

02

Why Stack Allocation Is O(1) (RSP Pointer Bump) vs Heap Allocators

The speed difference between stack and heap is the difference between sliding a bookmark and searching a warehouse:

  1. Stack Allocation (~1 ns):
    • The Pointer Bump: The CPU executes a single subtraction instruction: sub rsp, 64.
    • Instant Deallocation: When the function returns, it executes add rsp, 64.
    • Warm Cache Locality: Because the same small stack region is reused repeatedly, data stays pinned in the L1 data cache.
  2. Heap Allocation (~50โ€“100 ns):
    • Search & Binning: The allocator searches segregated free-lists (best-fit or first-fit) to locate an available chunk.
    • Concurrency Locks: If multiple threads allocate simultaneously, arena mutexes must be acquired.
    • Kernel Syscalls & Cache Misses: If bins are empty, the OS kernel must map pages via mmap or brk. Heap objects scatter across arbitrary RAM pages, frequently stalling CPU pipelines on cache misses.
C++23 snippet
// Stack: O(1) pointer bump, ~1ns, zero memory fragmentation
void stackFast() {
    int buffer[1024]; // Emits: sub rsp, 4096 (1 CPU instruction)
    buffer[0] = 42;
} // Emits: add rsp, 4096 (1 CPU instruction)

// Heap: ~50-100ns, mutex locks, potential OS page fault
void heapSlow() {
    int* buffer = new int[1024]; // Free-list search + chunk header
    delete[] buffer;
}
Interview takeaway:

Prefer stack allocation for fixed-size, short-lived objects; use heap allocation only for large buffers or objects with dynamic lifecycles.

03

new / delete vs malloc / free (Constructors, Types & std::bad_alloc)

The fundamental difference between C and C++ memory management comes down to object lifecycle:

  1. Raw Memory vs Living Objects: malloc only claims a chunk of raw bytes; it has no concept of classes, constructors, or invariants. new allocates bytes and immediately runs the constructor T::T(...).
  2. Destruction Guarantee: free releases bytes back to the OS without running destructors, leaking non-memory resources (like file handles or network sockets). delete invokes T::~T() before releasing memory.
  3. Failure Semantics: malloc returns NULL on failure, requiring manual checks. new throws std::bad_alloc on failure (unless configured with std::nothrow).
  4. Type Safety: malloc yields an untyped void* requiring a cast; new returns a strongly typed T* checked by the compiler.
C++23 snippet
struct Session {
    std::string user;
    Session(std::string u) : user(std::move(u)) {}
    ~Session() { std::println("Session closed"); }
};

// MALLOC: BUG! Destructor will never run; memory leaks!
Session* s1 = static_cast<Session*>(malloc(sizeof(Session)));
// new (s1) Session("alice"); // would need placement new
free(s1); // Destructor NOT called! user string leaks heap memory!

// NEW: Correct C++ lifecycle
Session* s2 = new Session("bob");
delete s2; // Calls ~Session() -> cleans up user string -> frees memory
Interview takeaway:

Never mix malloc/free with C++ classes; only new and delete invoke object constructors and destructors.

04

How delete[] Knows Element Count: The Hidden Array Cookie Trap

When you invoke delete[] ptr;, you do not pass the array length. How does the runtime know how many destructors to run?

  1. The Hidden Header (The Array Cookie): When T has a non-trivial destructor, new T[N] secretly allocates sizeof(size_t) + N * sizeof(T) bytes. The compiler writes the count N into the first 8 bytes and hands you a pointer shifted forward by 8 bytes (ptr = buffer + 8).
  2. How delete[] Unwinds: delete[] ptr looks backward at ((size_t*)ptr)[-1] to read N. It runs destructors on elements N-1 down to 0, then passes the true start address (ptr - 8) to the heap deallocator.
  3. The Mismatch Disaster: If you invoke scalar delete ptr on an array allocated with new[]:
    • It only calls the destructor on ptr[0], leaking the other N-1 elements.
    • It passes the interior pointer ptr instead of ptr - 8 to the heap free-list, corrupting allocator metadata and triggering a fatal process crash (SIGABRT).
C++23 snippet
struct Sensor {
    ~Sensor() { /* cleanup */ }
};

Sensor* arr = new Sensor[10]; // Allocates 8 bytes (cookie=10) + 10 * sizeof(Sensor)

// FATAL BUG: Undefined Behavior!
// delete arr; // Crashes allocator: passes arr instead of arr - 8!

delete[] arr; // Correct: reads arr[-1] == 10, calls 10 destructors, frees arr - 8
Interview takeaway:

Every new[] MUST be paired with delete[], and every scalar new with scalar delete; mixing them corrupts heap metadata.

05

Placement new & Explicit Destructor Calls

Standard new forces two distinct jobs together: finding memory and building an object. In performance-critical systems, you often want to separate them:

  1. The Separation of Concerns: Placement new constructs an object inside memory you already own (on the stack, in shared memory, or inside a pre-allocated pool) without calling the heap allocator:
    T* obj = ::new (static_cast<void*>(buffer)) T(args...);
    
  2. Never Call delete: Because placement new never called the heap allocator, calling delete obj attempts to return your private buffer to the OS free-list, causing immediate memory corruption.
  3. Explicit Destruction: To clean up, manually invoke the destructor: obj->~T() (or in C++20: std::destroy_at(obj)). The memory slot remains intact and ready for another object.
C++23 snippet
alignas(std::string) unsigned char storage[sizeof(std::string)];

// 1. Placement new: construct string inside stack buffer
std::string* s = ::new (static_cast<void*>(storage)) std::string("Zero Heap Allocation");

std::println("String: {}", *s);

// 2. Explicit destructor: mandatory cleanup!
s->~basic_string(); // In C++20: std::destroy_at(s);

// NEVER DO THIS: delete s; // CRASH! 'storage' is on the stack, not the heap!
Interview takeaway:

Use placement new to construct objects in pre-allocated memory; always destroy with an explicit destructor call ptr->~T(), never with delete.

04

Phase 1 (15-min baseline): FixedSlot<T>: Stack-Aligned In-Place Storage with Placement new & Manual Destruction

FixedSlot<T>: Stack-Aligned In-Place Storage with Placement new & Manual DestructionWrite this first in a 30-min interview

In a 30-minute interview on memory allocation and placement new, do not start by writing a 150-line generic growable vector with complex exception-rollback reallocation loops. Instead, write this clean, ~40-line FixedSlot<T> baseline in the first 12 minutes. It clearly isolates raw byte storage, alignment via alignas(T), placement new construction, and explicit destructor invocation with zero heap allocation before diving into dynamic capacity management.

Phase 1 โ€” 15-Minute Whiteboard Baseline (Simple & Interview-Safe)
#include <cstddef>
#include <iostream>
#include <new>
#include <string>
#include <utility>

template <typename T>
class FixedSlot {
    // 1. Raw byte storage strictly aligned to T's natural requirement
    alignas(T) unsigned char storage_[sizeof(T)];
    bool occupied_{false};

public:
    FixedSlot() noexcept = default;

    ~FixedSlot() {
        reset();
    }

    // In-place construction using placement new
    template <typename... Args>
    T* emplace(Args&&... args) {
        reset(); // Destroy existing object before overwriting
        T* ptr = ::new (static_cast<void*>(storage_)) T(std::forward<Args>(args)...);
        occupied_ = true;
        return ptr;
    }

    // Explicit destructor call (NEVER delete!)
    void reset() noexcept {
        if (occupied_) {
            reinterpret_cast<T*>(storage_)->~T();
            occupied_ = false;
        }
    }

    T* get() noexcept {
        return occupied_ ? reinterpret_cast<T*>(storage_) : nullptr;
    }

    const T* get() const noexcept {
        return occupied_ ? reinterpret_cast<const T*>(storage_) : nullptr;
    }
};

int main() {
    std::cout << "--- FixedSlot<std::string> Stack Placement New ---\n";
    FixedSlot<std::string> slot;
    std::cout << "slot.get() initially: " << (slot.get() ? *slot.get() : "nullptr") << '\n';

    slot.emplace("Placement New String in Stack Buffer");
    std::cout << "After emplace: " << *slot.get() << '\n';

    // Emplace again to verify reset() called before new placement
    slot.emplace("Replaced In-Place Object");
    std::cout << "After second emplace: " << *slot.get() << '\n';

    // Const accessor test
    const auto& constSlot = slot;
    if (constSlot.get()) {
        std::cout << "Const access: " << *constSlot.get() << '\n';
    }

    // Reset clears occupancy and invokes destructor
    slot.reset();
    std::cout << "After reset, slot.get(): " << (slot.get() ? "active" : "nullptr") << '\n';

    return 0;
}
Program output (stdout)
--- FixedSlot<std::string> Stack Placement New ---
slot.get() initially: nullptr
After emplace: Placement New String in Stack Buffer
After second emplace: Replaced In-Place Object
Const access: Replaced In-Place Object
After reset, slot.get(): nullptr
Starter traps & limitations interviewer will probe
  • Missing alignas(T) Causes Unaligned Bus Access: Declaring unsigned char storage_[sizeof(T)] without alignas(T) aligns only to 1 byte. If T contains a double or SIMD vector requiring 8-byte or 16-byte alignment, constructing it at an odd address causes throughput penalties on x86 or hardware bus faults (SIGBUS) on ARM.

  • Calling delete Instead of Explicit Destructor: Calling delete get() invokes the heap deallocator on stack memory, corrupting the heap free-list and crashing with free(): invalid pointer.

  • Overwriting Constructed Objects Without Prior Destruction: Calling placement new twice on the same memory without invoking ~T() causes resource leaks if T manages files, locks, or heap buffers.

  • Fixed Capacity Limitation: FixedSlot only manages a single object on the stack; it cannot scale dynamically to N elements without dynamic raw byte allocation via ::operator new.

The Senior Pivot Script (What to say at minute 15)

"This baseline demonstrates the fundamental separation of raw byte allocation, natural alignment, and object lifecycle. However, in production containers like std::vector, we must manage dynamic arrays on the heap where new T[cap] is unusable because it forces default construction. If you'd like, I can now pivot to Phase 2 and build a production-grade container buffer using ::operator new and placement new that supports arbitrary non-default-constructible types with zero-overhead reserve()."

05

Phase 2 (Production C++23): StackObjectSlot<T>: Stack-Allocated In-Place Object Pool with Placement new

StackObjectSlot<T>: Stack-Allocated In-Place Object Pool with Placement newCompiled & tested C++23

Implement a stack-backed StackObjectSlot<T> using alignas(T) unsigned char storage_[sizeof(T)] that constructs and destroys a noisy TrackedResource in-place using placement new and explicit destructor calls with zero heap allocation.

Implementation walkthrough
  1. 1.

    Define TrackedResource with static counters for default constructor, parameterized constructor, and destructor to trace object lifecycle.

  2. 2.

    Declare StackObjectSlot<T> with properly aligned stack storage alignas(T) unsigned char storage_[sizeof(T)] and an occupancy flag.

  3. 3.

    Implement emplace(Args&&... args): if the slot is currently occupied, invoke reset() first, then construct the new instance using placement new ::new (static_cast<void*>(storage_)) T(...).

  4. 4.

    Implement reset(): call the explicit destructor reinterpret_cast<T*>(storage_)->~T() and clear the occupancy flag.

  5. 5.

    Implement the slot's destructor ~StackObjectSlot() to automatically destroy any contained object when the slot leaves scope.

impl.cpp โ€” Live implementation (C++23)
#include <cstddef>
#include <cstdint>
#include <new>
#include <print>
#include <string>
#include <utility>

// ============================================================================
// 1. LIFECYCLE INSTRUMENTATION
// Tracks exact constructor and destructor execution to prove when memory
// is allocated vs when C++ object lifetime actually begins and ends.
// ============================================================================

struct TrackedResource {
    inline static int defaultCtorCount = 0;
    inline static int paramCtorCount = 0;
    inline static int dtorCount = 0;

    int id{0};
    std::string tag{};

    // Default constructor
    TrackedResource() : id(0), tag("default") {
        ++defaultCtorCount;
        std::println("  [TrackedResource] Default Ctor id={}", id);
    }

    // Parameterized constructor
    TrackedResource(int idVal, std::string tagVal) : id(idVal), tag(std::move(tagVal)) {
        ++paramCtorCount;
        std::println("  [TrackedResource] Param Ctor id={} tag={}", id, tag);
    }

    // Destructor
    ~TrackedResource() {
        ++dtorCount;
        std::println("  [TrackedResource] Dtor id={} tag={}", id, tag);
    }
};

// ============================================================================
// 2. STACK-BACKED PLACEMENT NEW STORAGE
// Pre-allocates raw uninitialized bytes on the CPU STACK with zero heap overhead.
// Demonstrates how std::optional, std::variant, and high-frequency systems work.
// ============================================================================

template <typename T>
class StackObjectSlot {
    // alignas(T) guarantees correct memory boundary alignment (e.g. 8-byte aligned)
    // unsigned char storage_ reserves raw uninitialized bytes without constructing T
    alignas(T) unsigned char storage_[sizeof(T)];
    bool occupied_{false}; // Tracks whether a live C++ object currently inhabits the slot

public:
    StackObjectSlot() noexcept = default;

    // Destructor: If an object exists, we MUST manually call its destructor!
    ~StackObjectSlot() {
        reset();
    }

    // Disable copies to prevent double-destruction bugs
    StackObjectSlot(const StackObjectSlot&) = delete;
    StackObjectSlot& operator=(const StackObjectSlot&) = delete;

    // Step 1: Placement new construction
    // Forwarding parameters directly into T's constructor inside our stack buffer
    template <typename... Args>
    T* emplace(Args&&... args) {
        if (occupied_) {
            reset(); // Clean up existing object before overwriting memory
        }

        // Placement new syntax: new (address) Type(arguments)
        // Performs NO dynamic heap allocation (malloc/operator new).
        // Only executes the constructor directly at &storage_[0].
        T* ptr = ::new (static_cast<void*>(storage_)) T(std::forward<Args>(args)...);
        occupied_ = true;
        return ptr;
    }

    // Step 2: Accessing the live object
    [[nodiscard]] T* get() noexcept {
        return occupied_ ? reinterpret_cast<T*>(storage_) : nullptr;
    }

    [[nodiscard]] const T* get() const noexcept {
        return occupied_ ? reinterpret_cast<const T*>(storage_) : nullptr;
    }

    // Step 3: Manual destructor invocation
    // Required because placement new did not allocate via heap 'new', so 'delete' CANNOT be called!
    void reset() noexcept {
        if (occupied_) {
            get()->~T(); // Explicit destructor call resets object state
            occupied_ = false;
        }
    }

    [[nodiscard]] bool hasValue() const noexcept { return occupied_; }
};

int main() {
    std::println("--- Stack vs Heap Placement New Demonstration ---");
    StackObjectSlot<TrackedResource> slot;

    // Emplace Alpha
    TrackedResource* pAlpha = slot.emplace(101, "Alpha");
    bool aligned = (reinterpret_cast<std::uintptr_t>(pAlpha) % alignof(TrackedResource) == 0);
    std::println("Emplaced Alpha in stack slot: correctly aligned = {}", aligned);
    std::println("Slot content: id={}, tag={}", slot.get()->id, slot.get()->tag);

    // Emplace Beta (overwriting Alpha)
    std::println("Emplacing Beta (triggers in-place destruction of Alpha):");
    slot.emplace(202, "Beta");
    std::println("Slot content: id={}, tag={}", slot.get()->id, slot.get()->tag);

    // Explicit reset
    std::println("Explicit reset of stack slot:");
    slot.reset();
    std::println("Slot occupied after reset = {}", slot.hasValue());

    int active = (TrackedResource::paramCtorCount + TrackedResource::defaultCtorCount) - TrackedResource::dtorCount;
    std::println("Lifecycle counts: paramCtors={}, dtors={}, active={}",
                 TrackedResource::paramCtorCount, TrackedResource::dtorCount, active);
    std::println("StackObjectSlot size: {} bytes, zero heap allocations",
                 sizeof(StackObjectSlot<TrackedResource>));

    return 0;
}
Program output (stdout)
--- Stack vs Heap Placement New Demonstration ---
  [TrackedResource] Param Ctor id=101 tag=Alpha
Emplaced Alpha in stack slot: correctly aligned = true
Slot content: id=101, tag=Alpha
Emplacing Beta (triggers in-place destruction of Alpha):
  [TrackedResource] Dtor id=101 tag=Alpha
  [TrackedResource] Param Ctor id=202 tag=Beta
Slot content: id=202, tag=Beta
Explicit reset of stack slot:
  [TrackedResource] Dtor id=202 tag=Beta
Slot occupied after reset = false
Lifecycle counts: paramCtors=2, dtors=2, active=0
StackObjectSlot size: 48 bytes, zero heap allocations
Complexity & implementation notes
  • emplace / reset: O(1) time, 0 heap allocations. Employs placement new directly on stack memory.
  • Alignment guarantee: Guaranteed aligned via alignas(T) to eliminate unaligned memory access penalties.
  • Storage size: Exactly sizeof(T) bytes plus boolean flag padding.
06

Interviewer cross-questioning & viva playbook

Q1Why new T[cap] Fails Without Default Constructor vs ::operator new + Placement new
Live Viva Grill
Interviewer asks / writes on screen

"Suppose struct Widget has only Widget(int id); with no default constructor. Why does new Widget[100] fail to compile, how does std::vector solve this, and how do ::operator new and placement new separate allocation from construction?"

Where candidates get trapped

Assuming new Widget[100] creates uninitialized memory like malloc, or thinking you can pass constructor arguments to array new[] for arbitrary capacities.

Senior mechanical explanation
  1. Array new[] Mandates Default Construction: In C++, new T[N] performs two steps in a single atomic statement: it invokes ::operator new[](N * sizeof(T) + cookie) to allocate raw heap bytes, and then immediately runs the default constructor T::T() on all N elements in order. If T lacks an accessible default constructor, compilation fails immediately with no matching function for call to 'Widget::Widget()'. Furthermore, even if T had a default constructor, allocating capacity for 10,000 slots would waste CPU cycles running 10,000 constructors immediately before any items are actually added!
  2. The Systems Solution โ€” Decoupling: Modern containers decouple memory allocation from object creation:
    • Step 1 (Raw Allocation): Allocate raw uninitialized bytes with ::operator new(cap * sizeof(T)). This allocates physical heap pages without calling any constructors.
    • Step 2 (In-Place Construction): When elements are added, use placement new (::new (static_cast<void*>(data + size)) T(args...) or std::construct_at). This constructs elements on demand directly in the raw buffer.
    • Step 3 (Explicit Destruction): When elements are removed or destroyed, invoke explicit destructors data[i].~T() (or std::destroy_at) without releasing the raw memory buffer.
Mechanical proof (C++)
struct Widget {
    int id;
    Widget(int x) : id(x) {} // NO default constructor!
};

// 1. FAILS:
// Widget* arr = new Widget[10]; // COMPILE ERROR: no default constructor!

// 2. PRODUCTION STL PATTERN:
// Step 1: Allocate uninitialized memory (0 constructors run)
Widget* buf = static_cast<Widget*>(::operator new(10 * sizeof(Widget)));

// Step 2: Construct on demand via placement new
::new (static_cast<void*>(buf + 0)) Widget(101);
::new (static_cast<void*>(buf + 1)) Widget(102);

// Step 3: Destroy in reverse order
buf[1].~Widget();
buf[0].~Widget();

// Step 4: Deallocate raw memory
::operator delete(buf);
Q2Why Calling delete on Placement-New Memory Crashes the Allocator
Live Viva Grill
Interviewer asks / writes on screen

"If you construct an object inside a pre-allocated buffer using Node* p = new (buffer) Node(42);, what happens if someone later executes delete p;? Walk through the runtime mechanics and heap manager internals."

Where candidates get trapped

Thinking delete p; simply runs the destructor and skips deallocation because it was placement new, or not knowing why the process crashes with SIGABRT.

Q3Alignment Requirements for Placement new Buffers (alignas vs SIGBUS)
Live Viva Grill
Interviewer asks / writes on screen

"Why is declaring char buffer[sizeof(T)] for placement new a critical bug? What is alignas(T), and what happens on modern CPUs if you construct an object in an unaligned buffer?"

Where candidates get trapped

Assuming that as long as the buffer has enough bytes (sizeof(buffer) >= sizeof(T)), memory can hold any type regardless of address alignment.

Q4The delete[] Array Cookie & Destructor Unwinding Mechanics
Live Viva Grill
Interviewer asks / writes on screen

"When you call delete[] ptr;, you do not pass the array size. How does the C++ runtime know how many destructors to run? What is the hidden array cookie, and what exact memory corruption occurs if you mix scalar delete with new[]?"

Where candidates get trapped

Believing the heap manager automatically knows how many C++ class destructors to call, or thinking delete[] and delete do the same thing under the hood.

07

Scenario: How std::vector Uses Raw Memory and Placement new to Avoid Default Construction

The interviewer's twist

The interviewer asks: "Why doesn't std::vector<T> use new T[capacity] when reserving buffer memory? How does it avoid default-constructing unused capacity slots, and how does it clean up elements upon destruction?"

How to handle this scenario

If std::vector used new T[capacity], it would be forced to run N default constructors immediately upon allocationโ€”wasting CPU cycles and completely failing for types that do not have a default constructor! Instead, std::vector allocates uninitialized raw memory via ::operator new(capacity * sizeof(T)) (or std::allocator_traits::allocate). As elements are added via push_back or emplace_back, it constructs them one by one using placement new. When elements are removed or the vector is destroyed, it calls explicit destructors only on the size_ initialized elements (data_[i].~T()), finally releasing the raw buffer with ::operator delete(data_).

scenario.cpp โ€” Scenario solution (C++23)
#include <cstddef>
#include <new>
#include <print>
#include <string>
#include <utility>

// ============================================================================
// SCENARIO: PREVENTING UNNECESSARY DEFAULT CONSTRUCTIONS (LIKE STD::VECTOR)
// Interview Twist: 'new T[N]' default-constructs N items immediately. If T has
// no default constructor or constructing is expensive, this breaks or wastes CPU.
// Solution: Allocate raw uninitialized bytes with ::operator new, then construct
// strictly on-demand using placement new.
// ============================================================================

struct Item {
    inline static int defaultCtorCalls = 0;
    inline static int paramCtorCalls = 0;
    inline static int dtorCalls = 0;

    int id{0};
    std::string name{};

    Item() : id(0), name("empty") {
        ++defaultCtorCalls;
    }

    Item(int idVal, std::string nameVal) : id(idVal), name(std::move(nameVal)) {
        ++paramCtorCalls;
        std::println("  [Item Ctor] Constructed id={} name={}", id, name);
    }

    ~Item() {
        ++dtorCalls;
        std::println("  [Item Dtor] Destroyed id={} name={}", id, name);
    }
};

// Mini raw buffer vector demonstrating how std::vector avoids default construction
template <typename T>
class RawVectorBuffer {
    T* data_{nullptr};
    std::size_t size_{0};
    std::size_t capacity_{0};

public:
    explicit RawVectorBuffer(std::size_t cap)
        : size_(0), capacity_(cap) {
        if (capacity_ > 0) {
            // Step 1: Allocate raw, uninitialized bytes with NO constructor calls!
            // ::operator new allocates sizeof bytes without running Type constructors.
            data_ = static_cast<T*>(::operator new(capacity_ * sizeof(T)));
        }
    }

    ~RawVectorBuffer() {
        clear(); // Destroy all constructed elements
        if (data_) {
            // Step 2: Deallocate raw memory with NO destructor calls on unused slots!
            ::operator delete(data_);
            data_ = nullptr;
        }
    }

    // Disable copies for clean RAII demonstration
    RawVectorBuffer(const RawVectorBuffer&) = delete;
    RawVectorBuffer& operator=(const RawVectorBuffer&) = delete;

    // Step 3: Construct on-demand at index 'size_' via placement new
    template <typename... Args>
    void emplace_back(Args&&... args) {
        if (size_ >= capacity_) {
            throw std::bad_alloc();
        }

        // Placement new constructs directly into the raw buffer slot
        ::new (static_cast<void*>(data_ + size_)) T(std::forward<Args>(args)...);
        ++size_;
    }

    // Step 4: Destroy the last active element via explicit destructor
    void pop_back() noexcept {
        if (size_ > 0) {
            --size_;
            (data_ + size_)->~T();
        }
    }

    // Step 5: Destroy only constructed elements in reverse order
    void clear() noexcept {
        while (size_ > 0) {
            --size_;
            (data_ + size_)->~T(); // Explicit destructor invocation
        }
    }

    [[nodiscard]] std::size_t size() const noexcept { return size_; }
    [[nodiscard]] std::size_t capacity() const noexcept { return capacity_; }
    [[nodiscard]] const T& operator[](std::size_t idx) const noexcept { return data_[idx]; }
};

int main() {
    std::println("1. Allocating RawVectorBuffer with capacity = 5:");
    {
        RawVectorBuffer<Item> vec(5);
        std::println("Buffer allocated: size={}, capacity={}", vec.size(), vec.capacity());
        std::println("Default ctor calls = {} (Zero unnecessary object initializations!)\n", Item::defaultCtorCalls);

        std::println("2. Emplacing 3 elements one by one via placement new:");
        vec.emplace_back(1, "Alpha");
        vec.emplace_back(2, "Beta");
        vec.emplace_back(3, "Gamma");

        std::println("\n3. Calling pop_back() to destroy the last active element:");
        vec.pop_back();
        std::println("After pop_back: size={}\n", vec.size());

        std::println("4. Exiting scope (destructor destroys only remaining active elements):");
    }

    std::println("\nFinal lifecycle counts: defaultCtors={}, paramCtors={}, dtors={}",
                 Item::defaultCtorCalls, Item::paramCtorCalls, Item::dtorCalls);
    return 0;
}
Program output (stdout)
1. Allocating RawVectorBuffer with capacity = 5:
Buffer allocated: size=0, capacity=5
Default ctor calls = 0 (Zero unnecessary object initializations!)

2. Emplacing 3 elements one by one via placement new:
  [Item Ctor] Constructed id=1 name=Alpha
  [Item Ctor] Constructed id=2 name=Beta
  [Item Ctor] Constructed id=3 name=Gamma

3. Calling pop_back() to destroy the last active element:
  [Item Dtor] Destroyed id=3 name=Gamma
After pop_back: size=2

4. Exiting scope (destructor destroys only remaining active elements):
  [Item Dtor] Destroyed id=2 name=Beta
  [Item Dtor] Destroyed id=1 name=Alpha

Final lifecycle counts: defaultCtors=0, paramCtors=3, dtors=3
08

Common C++ mistakes & UB traps

Mixing scalar delete with new[] (Corrupting the Heap Array Cookie)
Buggy / UB code
Widget* arr = new Widget[10];
delete arr; // UNDEFINED BEHAVIOR: corrupts heap metadata and leaks 9 objects!
Safe / idiomatic C++
Widget* arr = new Widget[10];
delete[] arr; // Correct: reads array cookie count and frees base allocation

new[] prefixes the array with an 8-byte count cookie. Scalar delete skips reading the cookie, destructs only arr[0], and passes the interior pointer arr to the allocator free-list instead of arr - 8, corrupting the heap allocator's free-list.

Calling delete on a Pointer Constructed via Placement new
Buggy / UB code
alignas(Node) unsigned char buf[sizeof(Node)];
Node* p = new (buf) Node(42);
delete p; // FATAL CRASH: passes stack address 'buf' to heap free()!
Safe / idiomatic C++
alignas(Node) unsigned char buf[sizeof(Node)];
Node* p = new (buf) Node(42);
p->~Node(); // Correct: explicit destructor only; buffer is reclaimed by stack

Placement new does not allocate memory from the heap; it uses existing storage. Calling delete p invokes ::operator delete(p), which attempts to return stack or pool memory to the OS heap manager, causing an immediate crash.

Forgetting alignas on Raw Byte Buffers for Placement new
Buggy / UB code
char buffer[sizeof(double)]; // char alignment is only 1 byte!
double* d = new (buffer) double(3.14); // UB: misaligned pointer access!
Safe / idiomatic C++
alignas(double) char buffer[sizeof(double)];
double* d = new (buffer) double(3.14); // Correctly aligned to 8 bytes

CPUs require types like double, long long, and SIMD registers to be aligned to multiples of their natural boundary (e.g. 8 or 16 bytes). Unaligned memory access can cause bus faults or significant CPU performance penalties.

Using malloc for Non-Trivially Copyable Types
Buggy / UB code
std::string* s = (std::string*)malloc(sizeof(std::string));
*s = "Hello"; // CRASH: assignment operator assumes string is already constructed!
Safe / idiomatic C++
std::string* s = new std::string("Hello"); // Constructs properly
delete s;

malloc allocates raw bytes without initializing object invariants (such as internal string pointers or capacities). Calling methods or assignment operators on unconstructed memory accesses garbage pointers, causing segmentation faults.

09

"What does this print / Why does this crash?"

Puzzle 1: Placement new Destructor Execution Order
Predict the behavior
struct Tracker {
    int id;
    Tracker(int i) : id(i) { std::print("C{} ", id); }
    ~Tracker() { std::print("D{} ", id); }
};

int main() {
    alignas(Tracker) char buf[sizeof(Tracker) * 2];
    Tracker* t1 = new (buf) Tracker(1);
    Tracker* t2 = new (buf + sizeof(Tracker)) Tracker(2);
    t1->~Tracker();
    t2->~Tracker();
}
Puzzle 2: Array Cookie Byte Offset Calculation
Predict the behavior
struct Echo {
    int val;
    Echo() : val(7) {}
    ~Echo() {}
};

int main() {
    Echo* arr = new Echo[4];
    std::size_t* cookie = reinterpret_cast<std::size_t*>(reinterpret_cast<char*>(arr) - sizeof(std::size_t));
    std::println("Cookie count = {}", *cookie);
    delete[] arr;
}
10

Rapid-fire interview Q&A

Why is stack allocation O(1) and deterministic, while heap allocation is non-deterministic?

What exact sequence of operations occurs when you execute Widget* w = new Widget(42);?

How does delete[] know how many elements to destruct, and why does mixing new[] with scalar delete cause crashes?

What is placement new, and why is calling delete on a placement new pointer undefined behavior?

Why can't std::vector use new T[capacity] when reserving capacity?

Community Doubts & Dynamic Notes

1 Verified Concepts
Click any term to locate its underlined occurrence in the chapter text
Interactive Terms in this Chapter (Hover to Inspect ยท Click to Locate in Text)
Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering