C++ Dynamic Memory (new/delete)
When you declare a normal variable in C++, it lives on the stack and vanishes automatically the moment it goes out of scope. But many real programs don’t know in advance how much memory they will need — the size of an array might depend on a value read at runtime, or an object might need to survive long after the function that created it returns. C++ solves this with dynamic memory allocation: the new and delete operators let you request memory from the heap while the program is running, and give you full control over exactly when that memory is released. Used well, dynamic memory unlocks flexible, long-lived data structures; used carelessly, it causes memory leaks, dangling pointers, and hard-to-debug crashes.
Overview: The Stack, the Heap, and new/delete
Every running C++ program manages two memory regions relevant to this lesson: the stack and the heap (also called the free store). The stack holds local variables and function call information. It is extremely fast, managed automatically by the compiler, and strictly scoped — when a function returns, every local variable it declared is destroyed instantly, in reverse order of creation. The catch is that the stack is limited and its lifetime is tied rigidly to scope: in standard C++ you cannot size a plain array with a runtime value, and you cannot make a stack variable outlive the function that created it.
The heap is different. It is a large pool of memory your program can request chunks from explicitly, at any time, in any amount, and those chunks remain reserved until you explicitly give them back — even long after the function that allocated them has returned. The new operator is how you ask for heap memory in C++:
new Typeallocates enough memory for one object ofType, runs its constructor if it has one, and returns a pointer to it.new Type[n]allocates enough contiguous memory fornobjects, runs the constructor for each element (for class types), and returns a pointer to the first element.
The pointer returned by new is your only handle to that memory. Unlike a stack variable, there is no name attached to the heap block itself — only the pointer variable that holds its address. If you overwrite that pointer without saving its value elsewhere, or let it go out of scope, you lose all ability to free the memory: this is a memory leak, and the memory stays reserved for the rest of the program’s run.
The delete operator is the reverse of new. delete p calls the destructor (for class types) and then returns the memory p points to back to the heap manager, making it available for future allocations. Arrays allocated with new[] must be freed with delete[] p, which calls the destructor for every element (not just the first) before releasing the whole block. Mixing them up — delete on an array, or delete[] on a single object — is undefined behavior.
Internally, new and delete sit on top of lower-level allocation functions (operator new and operator new[]), which manage a free list of reusable heap blocks much like C’s malloc/free. The key difference is that new is type-aware: it knows the size and type of what it’s allocating, it runs constructors and destructors automatically, and if memory is exhausted it throws a std::bad_alloc exception rather than silently returning a null pointer the way malloc does. (You can opt into the old behavior with new(std::nothrow), which returns nullptr on failure instead of throwing.)
Freshly allocated heap memory is not automatically zero-initialized the way some languages guarantee it. new int[5] gives you five ints full of leftover garbage until you assign to them; new int(0) zero-initializes a single int, and new int[5]() zero-initializes every element of the array.
Syntax
The general forms:
Type* ptr = new Type; // allocate one object
Type* ptr = new Type(args); // allocate one object, initialize it
Type* arr = new Type[n]; // allocate an array of n objects (n can be a runtime value)
delete ptr; // free a single object
delete[] arr; // free an array
| Form | What it does |
|---|---|
new Type |
Allocates memory for one Type on the heap, runs its constructor if it has one, and returns a Type*. |
new Type(args) |
Same, but forwards args to the constructor (or initializes a built-in type directly, e.g. new int(42)). |
new Type[n] |
Allocates a contiguous block for n objects; n may be a variable decided at runtime. Returns a Type* pointing at element 0. |
delete ptr |
Destroys the object ptr points to and frees its memory. Use only on memory from a non-array new. |
delete[] arr |
Destroys every element of the array and frees the whole block. Use only on memory from new Type[n]. |
new(std::nothrow) Type |
Like new, but returns nullptr on failure instead of throwing std::bad_alloc. |
One subtlety: after delete or delete[], the pointer variable still holds the old (now invalid) address — it does not automatically become nullptr. You must assign that yourself: ptr = nullptr;.
Examples
Example 1: A single dynamically allocated value
#include <iostream>
using namespace std;
int main() {
int* p = new int;
*p = 42;
cout << "Value: " << *p << endl;
delete p;
p = nullptr;
return 0;
}
Output:
Value: 42
new int reserves enough heap space for one int and hands back its address. We dereference p with *p to store and read the value, just like an ordinary variable. Once we’re done, delete p releases that memory back to the heap, and we set p to nullptr so it no longer points at freed memory.
Example 2: A dynamically sized array
#include <iostream>
using namespace std;
int main() {
int n = 5;
int* arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = (i + 1) * (i + 1);
}
cout << "Array: ";
int sum = 0;
for (int i = 0; i < n; ++i) {
cout << arr[i] << " ";
sum += arr[i];
}
cout << endl;
cout << "Sum of squares: " << sum << endl;
delete[] arr;
arr = nullptr;
return 0;
}
Output:
Array: 1 4 9 16 25
Sum of squares: 55
Here n could just as easily come from user input — the point is that new Type[n] lets the array’s size be decided while the program is running, something a plain C-style array cannot do. We index into the heap array with [] exactly like a stack array, then release the whole block at once with delete[].
Example 3: A realistic use case — a resizable inventory array
#include <iostream>
#include <string>
using namespace std;
struct Product {
string name;
double price;
};
Product* growArray(Product* oldArr, int oldCapacity, int newCapacity) {
Product* newArr = new Product[newCapacity];
for (int i = 0; i < oldCapacity; ++i) {
newArr[i] = oldArr[i];
}
delete[] oldArr;
return newArr;
}
int main() {
int capacity = 2;
int count = 0;
Product* inventory = new Product[capacity];
string names[4] = {"Widget", "Gadget", "Gizmo", "Doohickey"};
double prices[4] = {9.99, 19.99, 4.99, 14.99};
for (int i = 0; i < 4; ++i) {
if (count == capacity) {
capacity *= 2;
inventory = growArray(inventory, count, capacity);
cout << "Resized capacity to " << capacity << endl;
}
inventory[count].name = names[i];
inventory[count].price = prices[i];
++count;
}
cout << "Inventory:" << endl;
for (int i = 0; i < count; ++i) {
cout << " - " << inventory[i].name << ": $" << inventory[i].price << endl;
}
delete[] inventory;
inventory = nullptr;
return 0;
}
Output:
Resized capacity to 4
Inventory:
- Widget: $9.99
- Gadget: $19.99
- Gizmo: $4.99
- Doohickey: $14.99
This mirrors what containers like std::vector do internally: when the array runs out of room, a bigger block is allocated, the old elements are copied over, and the old block is freed with delete[]. Writing this by hand shows exactly why std::vector exists — it does this bookkeeping for you, safely and efficiently.
How It Works Step by Step (Under the Hood)
What happens during new Type[n]:
- The program asks the runtime for enough bytes to hold
nobjects ofType(for class types with non-trivial destructors, a little extra hidden “cookie” space is often reserved to remembern, sodelete[]later knows how many destructors to run). - The heap allocator searches its free list for a big-enough block, or asks the operating system for more memory if nothing fits.
- If the request cannot be satisfied,
operator newthrowsstd::bad_alloc(unless you usednothrow). - Once a block is reserved, the compiler generates code that runs
Type‘s constructor on each of thenelements, in order. - The address of the first element is returned and stored in your pointer.
What happens during delete[] arr:
- The compiler generates code to call
Type‘s destructor on each element, typically from last to first. - The block (including any cookie) is handed back to the heap allocator, which adds it to its free list so a future
newcan reuse it. - The memory is not zeroed or scrubbed — it may still contain the old values until something else overwrites it, which is exactly why touching a pointer after
deleteis dangerous.
Common Mistakes
Mistake 1: Forgetting to delete (memory leak)
void createLeak() {
int* p = new int(10);
// missing delete p; -- the memory is never freed once p goes out of scope
}
Every call to createLeak allocates memory that nothing ever frees. The pointer p disappears when the function returns, but the heap block it pointed to does not — it is now unreachable and permanently leaked until the program exits. In a long-running program (a server, a game loop) this steadily eats all available memory.
Corrected:
void noLeak() {
int* p = new int(10);
delete p;
}
Mistake 2: Mismatching new[] and delete
int* arr = new int[10];
for (int i = 0; i < 10; ++i) arr[i] = i;
// WRONG: this array was allocated with new[], so it must be
// freed with delete[]; plain delete is undefined behavior
// and can corrupt the heap or run only partial cleanup.
delete arr;
Corrected:
int* arr = new int[10];
for (int i = 0; i < 10; ++i) arr[i] = i;
delete[] arr;
arr = nullptr;
cout << "Array freed successfully" << endl;
Output:
Array freed successfully
The rule is simple but easy to forget: every new pairs with exactly one delete, and every new[] pairs with exactly one delete[]. Never mix the two forms.
Mistake 3: Using a pointer after it has been deleted (dangling pointer)
int* p = new int(5);
delete p;
// WRONG: p is now a dangling pointer. The memory it pointed
// to has been returned to the heap and may already be reused
// elsewhere; reading or writing through p is undefined behavior.
cout << *p << endl;
Corrected:
int* p = new int(5);
delete p;
p = nullptr;
if (p != nullptr) {
cout << *p << endl;
} else {
cout << "Pointer already freed, skipping access" << endl;
}
Output:
Pointer already freed, skipping access
Setting a pointer to nullptr immediately after delete turns a silent, unpredictable bug (reading garbage, corrupting memory, or crashing at random) into a checkable condition. It also protects against double delete: calling delete twice on the same non-null address is itself undefined behavior, but delete nullptr is explicitly guaranteed to be a safe no-op.
Best Practices
- Prefer
std::vector,std::string, and smart pointers (std::unique_ptr,std::shared_ptr) over rawnew/deletewhenever possible — they manage the matchingdeletefor you automatically, even when exceptions are thrown. - Always pair every
newwith exactly onedelete, and everynew[]with exactly onedelete[]. - Set a pointer to
nullptrimmediately after deleting it, and check fornullptrbefore dereferencing a pointer you didn’t just allocate. - Never delete the same non-null pointer twice, and never delete memory you didn’t allocate with
newyourself. - If a function allocates memory, document (or better, design) clearly who is responsible for freeing it — ambiguous ownership is the root cause of most leaks and double-frees.
- Use tools like AddressSanitizer or Valgrind during development to catch leaks and invalid accesses that are easy to miss by eye.
- Reach for raw
new/deletemainly when you’re implementing a low-level data structure yourself (like the resizable array in Example 3) — application code almost never needs it directly.
Practice Exercises
- Write a program that dynamically allocates an array of
doubleof a size you choose, fills it with values, computes and prints the average, and correctly frees the memory. - Take the following buggy function and fix it so it no longer leaks memory:
int* makeArray(int n) { int* a = new int[n]; for (int i = 0; i < n; ++i) a[i] = i * 2; return a; }combined with a caller that never callsdelete[]on the returned pointer. (Hint: the fix belongs in the caller, not the function itself — think about ownership.) - Extend the resizable inventory array from Example 3 so that, instead of doubling only when full, it also shrinks (allocates a smaller block and copies over) when
countdrops below one quarter ofcapacity. Print a message each time a resize happens, in either direction.
Summary
newanddeleteallocate and free memory on the heap at runtime, independently of function scope.new Type[n]lets you size an array at runtime; free it withdelete[], never plaindelete.- Forgetting to
deletecauses a memory leak; using a pointer afterdeletecauses undefined behavior (a dangling pointer). - Set pointers to
nullptrafter deleting them, and never delete the same pointer twice. newthrowsstd::bad_allocon failure by default;new(std::nothrow)returnsnullptrinstead.- Prefer
std::vectorand smart pointers in real code — they apply these same rules automatically and are far less error-prone than rawnew/delete.
