AlgoMaster Logo

Dynamic Memory Allocation (new, delete)

High Priority19 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Sometimes you need an object that outlives the function that created it, or whose existence depends on a runtime decision. C++ gives you new to ask the runtime for fresh memory and delete to give it back. This chapter covers the basics of new and delete for a single object. Arrays (new[]/delete[]) come in the next chapter, the failure modes (leaks, dangling pointers, double-delete) get their own chapter after that, and smart pointers, which most modern code uses to wrap raw new, come later.

Stack vs Heap: The Short Version

Every variable in your program lives in one of two main memory regions. Local variables and function parameters live on the stack: a chunk of memory that grows and shrinks as functions are called and returned. Stack memory is cheap and automatic. The compiler knows the size of each function's local variables at compile time, and it reclaims them the instant the function returns.

The heap is a different region, sometimes called the "free store" in C++ standardese. Heap memory is requested explicitly at runtime, and it stays alive until you explicitly release it. That's what new and delete are for.

Two things to take from the picture. First, stack memory is owned by the function that created it: once the function returns, the variables are gone. Second, heap memory is owned by whoever holds the pointer to it: it stays alive until someone calls delete on that pointer, regardless of which function is running.

A later section covers the stack and heap properly, including how they're laid out, why heap allocation is slower than stack allocation, and how memory fragmentation works. For this chapter, the short version is enough: new puts the object on the heap, and you have to remember to clean it up.

Why Dynamic Allocation Exists

Stack allocation handles most of what programs need. Three reasons motivate asking the runtime for heap memory.

Lifetime that outlives the current scope. If a function creates an object and wants the caller to keep using it, a stack variable won't work. The stack variable dies as soon as the function returns. The function needs to put the object somewhere with a longer lifetime, hand the caller a pointer, and let the caller decide when it's done.

Size known only at runtime. A stack array has to have a size the compiler knows at compile time. If a program asks the user "how many items?" and they answer at runtime, Item items[N]; won't work with N coming from std::cin. The program has to ask for memory at runtime, which means the heap. This applies to any object whose existence depends on runtime input.

Polymorphic storage. When a base class has several derived classes (the Inheritance and Polymorphism sections cover this), code often needs to hold a "thing of some derived type" without knowing the exact type at compile time. A Discount* can point at a PercentageDiscount, a FlatDiscount, or a BogoDiscount, and the right behavior runs at runtime. The actual derived object has to live somewhere with a stable address, and that somewhere is usually the heap.

Other reasons exist (sharing objects across owners, large objects that would overflow the stack), but these three are the most common.

new: Allocate and Construct

The basic form is:

new T does two things in one expression. It asks the runtime for enough memory to hold one T, and it default-initializes a T in that memory. The expression returns a pointer to the newly constructed T. You store that pointer somewhere, and from that moment on you can read and write the object through the pointer.

For built-in types like int, "default-initialized" means uninitialized: the int holds whatever was in that memory before. Reading *p before writing to it is undefined behavior. The code above is safe because it assigns 42 first, then reads.

For class types with a default constructor, new T runs the constructor:

The Product on the heap is fully constructed before new returns. Its name is the default "unknown" and its price is 0.0, because that's what the default constructor (generated from the in-class initializers) sets them to.

For zero-initialized built-ins instead of garbage, use the parentheses or brace form with no arguments:

This matters for int, double, bool, raw pointers, and other built-in types. For class types with a default constructor, new T; and new T(); produce the same result.

new T(args) and new T{args}: Constructor Arguments

To create a heap object with specific values, pass constructor arguments. C++ gives you two syntaxes that do almost the same thing.

The parenthesized form calls a matching constructor:

The brace-init form uses list-initialization, which has slightly stricter rules:

For most types either form produces the same result. The brace form refuses narrowing conversions (for example, new int{3.5} is a compile error, while new int(3.5) truncates to 3 without a warning), which makes it a safer default. Either form is fine in the rest of the chapter.

A concrete example. A Product has a two-argument constructor, and we build one on the heap:

new Product("Wireless Mouse", 24.99) asks the runtime for enough memory to hold a Product, calls the two-argument constructor in that memory, and returns a Product*. The same goes for the brace-init form on the next line. Both pointers are valid until delete runs.

A new expression is much more expensive than a stack allocation. Stack allocation moves the stack pointer, which is a single instruction. new typically calls into the standard library's allocator, which walks data structures to find a free block, possibly takes a lock for thread safety, and returns. Heap allocation can be 10 to 100 times slower than stack allocation, depending on the allocator and contention. Don't new something that could be a local variable.

delete: Destroy and Free

Every new you write should be paired with exactly one delete. The delete expression does two things: it runs the object's destructor (for class types), and it returns the memory to the runtime.

After delete p;, the memory p used to point at is no longer yours. Reading or writing through p from that moment on is undefined behavior, even though the bits stored in p itself are unchanged. The pointer still holds the same address it did before, but that address now belongs to the runtime. This stale pointer is called a dangling pointer.

Two safe properties of delete:

  • delete nullptr; is a no-op. The standard guarantees this, so you don't need to write if (p) delete p; defensively.
  • Calling delete on the same non-null pointer twice is undefined behavior. Don't.

A Product example that shows the destructor running:

The constructor runs as part of the new expression, the destructor runs as part of delete. The order is: construct, use, destroy, continue. Constructors and destructors are covered in the Object-Oriented Programming section; for now, treat them as "code that runs when an object is created" and "code that runs when an object is destroyed".

One new, One delete: Ownership

The rule for raw new/delete is: every successful new must be matched by exactly one delete on the same pointer. Miss the delete and the program leaks. Call delete twice and the program has undefined behavior. Call delete on a pointer that didn't come from new and the program has undefined behavior.

That "exactly one" is the hard part. In a small example with one function, it's easy. In code with multiple functions, branches, early returns, and exceptions, it's easy to slip up. A pointer gets handed from one function to another, and at some point no one is sure whether delete has already run or whether it still needs to.

The C++ term for this is ownership. At any moment, some piece of code is the owner of a heap object, meaning that piece of code is responsible for eventually calling delete. Other code might hold the pointer too, but only one of them owns the cleanup.

A pattern this chapter is teaching you to handle, framed around an e-commerce checkout flow:

createProduct allocates a Product and returns a pointer to it. main is now the owner. It uses the product, then deletes it. printProduct takes a pointer but doesn't own anything: it reads through the pointer and returns. The convention, "functions don't delete a pointer they didn't allocate themselves unless the API explicitly says so", is what keeps the ownership story coherent.

The rule "one new, one delete" turns into a small set of patterns: who creates, who passes around, who eventually deletes. Smart pointers (in the Memory Management section) automate this. Until then, document the ownership in comments and follow it carefully.

Lifetime That Outlives the Scope

The first reason to use new is when an object needs to live beyond the function that created it. A heap object lives until someone calls delete, regardless of which functions return in the meantime.

An order-lookup example. The createOrder function builds an Order, registers it in a small lookup table, and returns. The Order stays alive because the table still has a pointer to it. Other functions can find the order by ID, modify it, and delete it when the order is finalized.

Step through it. createOrder runs and returns, but the Order it built is still alive because orderTable holds a pointer to it. findOrder reads the pointer back out of the table. finalizeOrder is the function that owns the cleanup: it deletes the Order and clears the table slot so nothing tries to use the dangling pointer later.

If createOrder had built the order on the stack (Order o{id, customer, total};) and stored its address in the table, the address would be dangling the moment createOrder returned. The heap version works because the object's lifetime is decoupled from any function's stack frame.

Polymorphic Storage

The third reason for new is storing objects whose exact type depends on a runtime choice. Inheritance and polymorphism are full sections by themselves, so this is a quick preview.

Consider an e-commerce site that supports several kinds of discount, where the choice depends on a configuration that changes at runtime. A Discount base class declares the interface, and PercentageDiscount and FlatDiscount implement it differently. A pointer to Discount can hold either kind, and the right apply runs through virtual functions.

chooseDiscount returns a Discount* that might point to a PercentageDiscount, a FlatDiscount, or be nullptr. The exact type is decided at runtime based on the string. main doesn't know which derived class it got; it calls apply, and the virtual dispatch picks the right implementation.

The reason this needs the heap is that the derived object must live somewhere with a stable address, and that address must survive past the function that decided which type to create. The base-class pointer can't point at a stack variable inside chooseDiscount because that variable dies on return. new gives the derived object a home that outlives the decision-making function.

The other important detail is the virtual ~Discount() = default; line. When delete runs through a base-class pointer, the derived class's destructor should run too. The destructor has to be virtual for that to work. The rule: polymorphic base classes need a virtual destructor.

What Happens When Allocation Fails

new asks the runtime for memory, and the runtime might not have any. On a typical desktop machine with gigabytes of RAM, this is rare, but it does happen: very large allocations, embedded targets with tight memory, or fragmented allocators that can't find a contiguous block.

By default, when new can't satisfy the request, it throws std::bad_alloc. The program either catches the exception or terminates with an uncaught exception. The try-catch is rarely needed for everyday code, because if the program can't allocate a few hundred bytes, it probably can't recover anyway.

Output (on most systems):

For use cases where allocation failure is expected and handled without exceptions, C++ offers a nothrow form:

new(std::nothrow) T returns nullptr on failure instead of throwing. The caller checks the pointer before using it. The nothrow form is uncommon in modern code, because exceptions are the standard mechanism, but it's good to recognize.

Catching std::bad_alloc doesn't slow down new on the success path. The exception is only thrown on failure. The cost is the same as a normal allocation, plus exception-handling cost when it fails.

A Note on new as an Expression

The C++ standard describes new T as two steps. First, the runtime calls operator new, a function that returns raw memory of the right size. Second, the compiler runs T's constructor in that memory. The pointer returned is the address of the constructed object.

For everyday code the split doesn't matter. new T is one expression that does both steps. The split matters in three places that are out of scope for this chapter:

  • Custom allocators (Memory Management section) let you replace the memory-fetching step without changing the construction.
  • Placement new (Memory Management section) is a separate form that runs the constructor in memory you already have, skipping the allocation.
  • Operator new overloading (Memory Management section) lets a class change how its instances are allocated.

For now, read new T(args) as "allocate one T on the heap, construct it with these args, return a pointer to it".

Common Pitfalls: A Preview

A lot can go wrong with raw new and delete. Three failure modes show up repeatedly:

MistakeWhat happens
Forgot to deleteMemory leaks; program slowly grows
Deleted twiceUndefined behavior; usually a crash or heap corruption
Used after deleteUndefined behavior; reads garbage or crashes

This chapter introduced the mechanics. Smart pointers (std::unique_ptr, std::shared_ptr) make most of these mistakes structurally impossible. In modern C++, raw new is almost always wrapped in a smart pointer rather than paired with a hand-written delete.

For this chapter, the rule is the simplest one: every new needs exactly one delete. Train your eye to look for that pairing.

Putting It Together

An example that pulls the chapter's ideas together. A catalog of products, and the user picks one to feature. The featured product's data is built at runtime based on the choice, lives beyond the function that created it, and gets cleaned up at the end.

A few details. The Desk Lamp is constructed inside buildFeatured, used inside main, and destroyed when main calls delete featured. The lifetime spans three function calls. buildFeatured("does-not-exist") returns nullptr, and delete missing is safe even though no allocation ever happened, because deleting null is a no-op. The constructor and destructor messages bracket the object's lifetime, which is the picture to keep in mind whenever you write new and delete.

Quiz

Dynamic Memory Allocation Quiz

10 quizzes