Stack vs Heap, Placement new & Memory Layout
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).
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.
The expression new Widget(42) is a C++ language expression that executes two distinct steps:
- Memory Allocation: The compiler calls the allocation function
void* mem = ::operator new(sizeof(Widget)). If allocation fails, it invokes thestd::new_handleror throwsstd::bad_alloc. - Object Construction: The compiler runs the constructor
Widget::Widget(42)in-place at addressmem. If the constructor throws an exception, the runtime automatically calls::operator delete(mem)to prevent a memory leak before propagating the exception.
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.
Interviewer's lens
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:
- 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). - The Array Cookie Contract: How
delete[]reads the hidden 8-byte prefix cookie (ptr[-1]) to determine destructor iterations, and why mixing scalardeletecorrupts allocator headers. - Decoupled Lifecycle: How placement new separates raw memory allocation from object construction, empowering low-latency memory pools and STL containers.
- Instruction-Level Mechanics: Why allocating stack memory is a single CPU subtraction (
Under the hood & memory layout
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:
- 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. - The Heap Dance: Standard
new Tperforms two distinct operations: it calls::operator newto search allocator free-lists for a suitable chunk, then calls the constructorT::T(...)to initialize the object. - 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.
.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).
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 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.
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 (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.
Core concepts & syntax traps
Process Memory Segments (.text, .rodata, .data, .bss, Heap, Stack)
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:
- 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.
- 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).
- The Heap (Grows Upward): Shared pool managed by runtime allocators (ptmalloc, jemalloc) via kernel requests (
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;
}Globals and statics live in .data/.bss for the program lifetime; local variables live on the Stack; dynamically allocated memory lives on the Heap.
Why Stack Allocation Is O(1) (RSP Pointer Bump) vs Heap Allocators
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:
- 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.
- The Pointer Bump: The CPU executes a single subtraction instruction:
- 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
mmaporbrk. Heap objects scatter across arbitrary RAM pages, frequently stalling CPU pipelines on cache misses.
// 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;
}Prefer stack allocation for fixed-size, short-lived objects; use heap allocation only for large buffers or objects with dynamic lifecycles.
new / delete vs malloc / free (Constructors, Types & std::bad_alloc)
new / delete vs malloc / free (Constructors, Types & std::bad_alloc)
The fundamental difference between C and C++ memory management comes down to object lifecycle:
- Raw Memory vs Living Objects:
malloconly claims a chunk of raw bytes; it has no concept of classes, constructors, or invariants.newallocates bytes and immediately runs the constructorT::T(...). - Destruction Guarantee:
freereleases bytes back to the OS without running destructors, leaking non-memory resources (like file handles or network sockets).deleteinvokesT::~T()before releasing memory. - Failure Semantics:
mallocreturnsNULLon failure, requiring manual checks.newthrowsstd::bad_allocon failure (unless configured withstd::nothrow). - Type Safety:
mallocyields an untypedvoid*requiring a cast;newreturns a strongly typedT*checked by the compiler.
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 memoryNever mix malloc/free with C++ classes; only new and delete invoke object constructors and destructors.
How delete[] Knows Element Count: The Hidden Array Cookie Trap
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?
- The Hidden Header (The Array Cookie):
When
Thas a non-trivial destructor,new T[N]secretly allocatessizeof(size_t) + N * sizeof(T)bytes. The compiler writes the countNinto the first 8 bytes and hands you a pointer shifted forward by 8 bytes (ptr = buffer + 8). - How
delete[]Unwinds:delete[] ptrlooks backward at((size_t*)ptr)[-1]to readN. It runs destructors on elementsN-1down to0, then passes the true start address (ptr - 8) to the heap deallocator. - The Mismatch Disaster:
If you invoke scalar
delete ptron an array allocated withnew[]:- It only calls the destructor on
ptr[0], leaking the otherN-1elements. - It passes the interior pointer
ptrinstead ofptr - 8to the heap free-list, corrupting allocator metadata and triggering a fatal process crash (SIGABRT).
- It only calls the destructor on
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 - 8Every new[] MUST be paired with delete[], and every scalar new with scalar delete; mixing them corrupts heap metadata.
Placement new & Explicit Destructor Calls
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:
- The Separation of Concerns:
Placement
newconstructs 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...); - Never Call
delete: Because placement new never called the heap allocator, callingdelete objattempts to return your private buffer to the OS free-list, causing immediate memory corruption. - 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.
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!Use placement new to construct objects in pre-allocated memory; always destroy with an explicit destructor call ptr->~T(), never with delete.
Phase 1 (15-min baseline): FixedSlot<T>: Stack-Aligned In-Place Storage with Placement new & Manual Destruction
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.
#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;
}--- 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
Missing alignas(T) Causes Unaligned Bus Access: Declaring
unsigned char storage_[sizeof(T)]withoutalignas(T)aligns only to 1 byte. IfTcontains adoubleor 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 withfree(): invalid pointer.Overwriting Constructed Objects Without Prior Destruction: Calling placement new twice on the same memory without invoking
~T()causes resource leaks ifTmanages files, locks, or heap buffers.Fixed Capacity Limitation:
FixedSlotonly manages a single object on the stack; it cannot scale dynamically to N elements without dynamic raw byte allocation via::operator new.
"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()."
Phase 2 (Production C++23): StackObjectSlot<T>: Stack-Allocated In-Place Object Pool with Placement new
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.
- 1.
Define
TrackedResourcewith static counters for default constructor, parameterized constructor, and destructor to trace object lifecycle. - 2.
Declare
StackObjectSlot<T>with properly aligned stack storagealignas(T) unsigned char storage_[sizeof(T)]and an occupancy flag. - 3.
Implement
emplace(Args&&... args): if the slot is currently occupied, invokereset()first, then construct the new instance using placement new::new (static_cast<void*>(storage_)) T(...). - 4.
Implement
reset(): call the explicit destructorreinterpret_cast<T*>(storage_)->~T()and clear the occupancy flag. - 5.
Implement the slot's destructor
~StackObjectSlot()to automatically destroy any contained object when the slot leaves scope.
#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;
}--- 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
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.
Interviewer cross-questioning & viva playbook
"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?"
Assuming new Widget[100] creates uninitialized memory like malloc, or thinking you can pass constructor arguments to array new[] for arbitrary capacities.
- 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 constructorT::T()on all N elements in order. IfTlacks an accessible default constructor, compilation fails immediately withno matching function for call to 'Widget::Widget()'. Furthermore, even ifThad a default constructor, allocating capacity for 10,000 slots would waste CPU cycles running 10,000 constructors immediately before any items are actually added! - 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...)orstd::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()(orstd::destroy_at) without releasing the raw memory buffer.
- Step 1 (Raw Allocation): Allocate raw uninitialized bytes with
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);"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."
Thinking delete p; simply runs the destructor and skips deallocation because it was placement new, or not knowing why the process crashes with SIGABRT.
"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?"
Assuming that as long as the buffer has enough bytes (sizeof(buffer) >= sizeof(T)), memory can hold any type regardless of address alignment.
"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[]?"
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.
Scenario: How std::vector Uses Raw Memory and Placement new to Avoid Default Construction
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?"
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_).
#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;
}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
Common C++ mistakes & UB traps
Widget* arr = new Widget[10]; delete arr; // UNDEFINED BEHAVIOR: corrupts heap metadata and leaks 9 objects!
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.
alignas(Node) unsigned char buf[sizeof(Node)]; Node* p = new (buf) Node(42); delete p; // FATAL CRASH: passes stack address 'buf' to heap free()!
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.
char buffer[sizeof(double)]; // char alignment is only 1 byte! double* d = new (buffer) double(3.14); // UB: misaligned pointer access!
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.
std::string* s = (std::string*)malloc(sizeof(std::string)); *s = "Hello"; // CRASH: assignment operator assumes string is already constructed!
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.
"What does this print / Why does this crash?"
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();
}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;
}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?