Why this matters
So far every variable in this module has lived in a fixed place chosen by the compiler, and it disappeared when its block ended. That is fine for a counter or a short array. It is not enough for real programs. You often do not know how many items you need until the program runs. A list of search results might hold 3 entries or 3 million. A function might build a structure and return it to its caller, which must still exist after the function is gone. For these cases C++ gives you the heap, a region of memory that you ask for explicitly and that lasts until you give it back.
Heap memory brings responsibility. Every block you ask for must be returned exactly once. Forget, and the program slowly eats memory. Return it twice, or keep using it after it is returned, and the program corrupts itself. Most of the hard bugs in C++ come from these rules, and most interview questions about memory are checks that you know them. This lesson teaches the two kinds of memory, the operators new and delete, and the habits that keep your programs correct.
Two places for memory
Think of a program's memory as two storage areas with different rules.
The first area is the stack. A good picture is a stack of plates in a kitchen. You put a plate on the top, and you take the top plate off. You cannot take a plate from the middle. When a function starts, the program puts a block of memory for it on top of the stack. That block is called a frame. It holds the function's parameters and its local variables. When the function returns, its frame is removed, and everything in it disappears at once. The stack is fast, because adding and removing a frame is only a matter of moving one marker. You never ask for stack memory by name. The compiler decides the size, and the rules end it for you.
The second area is the heap. A good picture is a warehouse. You go to the desk, ask for a shelf of a certain size, and receive the address of the shelf. The warehouse keeps the shelf for you until you return it. Nothing removes your shelf when a function ends. Only your delete returns it. Because you control the lifetime, you can build data that outlives the function that created it. You also pay a price: the warehouse has to find free shelves, and if you forget to return one, the shelf stays occupied for the life of the program.
Variables you declare in a function, such as int x = 3;, live on the stack. Memory you ask for with new lives on the heap. The rest of this lesson turns on that difference.
Stack frames for main and f on the left, and a heap block of three ints on the right, with p pointing into the heap
The call stack
Each time a function is called, the program pushes a frame on the stack. When the function returns, the program pops the frame off. If main calls f and f calls g, the stack holds three frames at the deepest point, one for each function still running. This record of frames is called the call stack.
The top of the stack is the first box. The call on top is the one running. A filled box is returning a value.
Call factorial(4). It goes on top of the stack.
Step through the call stack above. Watch the frames appear as each call starts and disappear as each call returns. This is the same stack that holds your local variables. A variable in g disappears when g returns, and the address of that variable is then invalid. That is the reason you cannot return a pointer or a reference to a local variable, a rule you already saw in lesson 3.
Notice what the picture shows about lifetime. Frames live exactly as long as the calls that made them. A frame on the stack can never outlive its function. The heap has no such rule. Its blocks live until someone deletes them.
The operator new
The new operator asks the heap for memory big enough for one object of a type. It returns the address of that memory, as a pointer. Here is the simplest use.
int* p = new int;The line does three things. It asks the heap for 4 bytes, which is the size of an int. It hands back the address of those bytes. And it stores that address in p. The 4 bytes have no name. The only way to reach them is through p.
The value inside a plain new int is not set. It holds whatever bytes were there before, which is garbage. To give it a value, use parentheses:
int* p = new int(42);Now the 4 bytes hold 42. The parentheses ask for initialisation, and the new form that takes a value is the one you will use most. For a class type, such as a node in a linked list, new calls the constructor you wrote, which is how you set the fields.
The operator delete
When you are done with a heap block, return it with delete. The rule is that every pointer returned by new must be passed to delete exactly once, and the pointer must still hold the address that new gave you.
delete p;
p = nullptr;The delete returns the memory to the heap. It does not change the variable p. After the call, p still holds the old address, but that address is no longer yours to use. Setting p to nullptr right after the delete makes the old address impossible to reach by accident. This habit is cheap, and it turns a silent bug into a clear crash that you can find.
Applying delete to nullptr does nothing and is safe. So a pointer that might be empty can always be deleted without a test.
Worked example 1: one int on the heap
Here is a complete program that uses one heap int.
Output:
value: 42
after change: 50
freed and clearedTrace the lifetime. The new in the first statement creates the block and gives it 42. The change in the third statement writes 50 into the same block. The delete in the fifth statement returns the block. The next statement, p = nullptr, removes the old address from p. After that point the block belongs to the heap again, and nothing in the program can reach it.
The program is short, so the lifetime is obvious. In a real program the new and delete may be in different functions, separated by many lines. The point of this example is the shape: one new, one delete, and clear ownership between them.
Worked example 2: an array on the heap
Heap arrays are the most common use of new. Their size can come from input. The operator is new int[n], and the matching delete is delete[].
Input:
4
10 20 30 40Output:
count 4, sum 100The array size n is not known at compile time, so a stack array such as int values[n] would not be standard C++. The heap solves that. The sum uses long long, which holds larger totals than int. That habit is worth keeping, because the sum of many large numbers can overflow an int even when each number fits.
The key line is delete[] values;. The square brackets are required. A new[] must be matched with delete[]. Mixing them up is undefined behaviour, and the sanitizer in the mistakes section shows the error. For a plain int array the damage is often invisible, but for arrays of objects with destructors, the wrong form can skip the destructors and leak resources.
Worked example 3: nodes linked together
Here is the structure you will see in every linked-list lesson. Each node is allocated on the heap, and each one holds a pointer to the next. The nodes are not in one block. They are scattered wherever the heap found room, and the pointers link them.
Output:
1 2 3
all nodes freedThe first loop walks the list by following next. The second loop frees the nodes, and it has a detail that matters. It saves cur->next into nextNode before deleting cur. If it read cur->next after the delete, it would read a freed node, which is a use-after-free bug. The rule is to read everything you need from a node before you return it to the heap.
Use the -> arrow to reach a field through a pointer. cur->value means (*cur).value. The arrow is shorthand, and every linked-list program uses it.
Worked example 4: a function that returns a heap pointer
A function can create heap memory and return its address, so the caller uses it after the function ends. The caller then owns the memory and must delete it.
Output:
counter = 13This is the legal version of the mistake from lesson 3. The function returns a pointer to heap memory, not to its own local. The heap block survives the return, so main can use it. The price is that the caller has to remember the delete. Good code makes that duty obvious, either by naming the function makeCounter so a reader expects an allocation, or, in modern code, by returning an owning smart pointer as lesson 5 shows.
Here is a second example with an array, where the function builds the result and the caller frees it.
Output:
1 4 9 16 25The array holds the squares of 1 through 5. It lives on the heap because squares returns it. When main finishes using it, delete[] s releases it.
Worked example 5: new with value initialisation
The plain form new int[5] leaves the five values unset. Adding empty parentheses sets them to zero. This is called value initialisation, and it is the safer choice when you will read the values before writing them.
Output:
0 0 0The plain array is not printed, because its values are garbage. Reading garbage is undefined behaviour, so a correct program never does it. The zeroed array is guaranteed to start at zero. Use new int[n]() when you need a zeroed array of a given size, and remember the trailing parentheses.
Tracing heap use on paper
The table method works for heap memory too, and it is the clearest way to see when a block is alive. Take this loop:
for (int i = 0; i < 3; i++) {
int* p = new int(i * 10);
cout << *p << endl;
delete p;
}Trace each iteration with five columns: what new gave, how many blocks are live after new, the output, and how many are live after delete.
| Iteration | new gives | Live blocks after new | Output | Live blocks after delete |
|---|---|---|---|---|
| 0 | block A, holding 0 | 1 | 0 | 0 |
| 1 | block B, holding 10 | 1 | 10 | 0 |
| 2 | block C, holding 20 | 1 | 20 | 0 |
The count of live blocks never goes above one, because each block is freed before the next is made. The allocator may even hand back the same address each time, since it reuses freed memory. Do not rely on that. The important part is the pattern. Each row pairs one new with one delete, and the count returns to zero.
Now remove the delete line. The live count after each row becomes 1, 2 and 3, and the program ends with three live blocks that nothing can reach. That is a leak, and the table shows it before you run anything.
What happens inside the heap
When you call new, the C++ runtime asks an allocator to find a free block of the right size. The allocator keeps records of which parts of the heap are free and which are in use. It stores a small header with each block, usually a few bytes, that records the block's size. That header is how delete knows how much memory to return. You never see it, but it is why delete does not need you to say the size.
The allocator usually does not give memory back to the operating system when you call delete. It keeps the freed block in a list and reuses it for the next new of a similar size. This is why a program that allocates and frees in a loop does not grow without limit, and why memory use can seem to stay high after you free something. The memory is free for your program, even though the operating system still shows it as in use.
When a program ends, the operating system takes back all of its memory, including any heap blocks you forgot to delete. That is why a small test program with a leak seems to work fine. The leak only matters when a program runs for a long time, or when it creates leaks many times in a loop. Do not rely on the operating system to clean up for you. Treat every leak as a bug, even when the output looks right.
What new costs, compared with the stack
Allocating on the stack costs almost nothing. The program moves one marker to make room for a frame, and moves it back when the function returns. A heap allocation is slower. The allocator searches its records for a free block of the right size, splits a larger block if needed, writes the header, and later merges freed blocks back together. That work takes time, and the amount depends on the program's history, so two identical calls to new can take different times.
Speed is not the only cost. Heap blocks are scattered across memory, so a program that follows many pointers jumps between distant addresses. Each jump can miss the processor's cache, which is a small, fast memory close to the processor. A loop over a vector runs much faster than the same loop over a linked list of heap nodes, even with the same number of steps, because the vector's elements sit side by side. This is why performance answers in interviews often prefer arrays to pointer-based lists.
The practical rule is simple. Allocate on the stack when the size is small and fixed. Use a vector when the size varies and the data is one collection. Use new and smart pointers for objects whose lifetime does not match a function call, and for structures such as linked lists and trees, whose parts are joined by pointers.
A leak has a cost too, but the cost arrives later. A leak does not slow a short program. A server that leaks a few bytes per request runs out of memory after days, which is why leaks survive testing and why the sanitizer is the best tool to catch them.
Ownership rules in one list
Every heap block in this lesson had exactly one owner, and the owner was the code that freed it. Four rules keep that true.
- Each
newhas exactly one matchingdelete, and eachnew[]has onedelete[]. - Copying a pointer does not create an owner.
int* q = p;gives a second slip of paper, not a second right to free the block. - A function that returns a pointer from
newtransfers ownership to its caller. The caller must free the block, or pass the duty on. - Every path out of a function must follow rules 1 to 3. A
returnin the middle of a function is a path, and it must free whatever the function owns.
These rules are easy in small programs and hard in long ones, where a pointer is copied into a structure and the copy outlives the original. Lesson 5 turns the rules into types, so the compiler can check some of them for you. Until then, write the delete at the same time as the new, and walk every return path before you call a function finished.
Common mistakes and the messages you see
The first mistake is a leak: a new with no matching delete. Here is a small program that makes one:
#include <iostream>
using namespace std;
int main() {
int* a = new int[10];
a[0] = 1;
cout << a[0] << endl;
return 0;
}The program prints 1 and exits normally. Compiled with the address sanitizer, which checks memory and reports leaks, it prints this report when it finishes. The report is shortened here, and the addresses are left out:
=================================================================
==1983611==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 40 byte(s) in 1 object(s) allocated from:
#0 operator new[](unsigned long)
#1 in main leak.cpp:3The line leak.cpp:3 points to the new. Forty bytes is ten ints. The fix is to add delete[] a; before the return.
The second mistake is use after free. This program deletes a block and then reads it:
#include <iostream>
using namespace std;
int main() {
int* p = new int(3);
delete p;
cout << *p << endl;
return 0;
}Compiled with -fsanitize=address, the first lines of the report are:
=================================================================
==1983514==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 4 at 0x... thread T0
#0 in main uaf.cpp:5
0x... is located 0 bytes inside of 4-byte region
freed by thread T0 here:
#1 in main uaf.cpp:4Read the report from the top. It says the read happened on line 5, and the block was freed on line 4. Without the sanitizer, the same program might print 3, print garbage or crash, and the result can change from run to run. Set p = nullptr after the delete, and the bad read crashes at once, which is easier to find.
The third mistake is a double free: deleting the same block twice. Here the second delete tries to return memory the heap has already taken back. Depending on the compiler and the allocator, the error can look like this from the C library:
free(): double free detected in tcache 2The fix is to make sure each block has one owner, and that the owner deletes it once. Setting the pointer to nullptr after the first delete also helps, because the second delete then does nothing.
The fourth mistake is mismatched forms. Calling delete on a pointer from new[] is undefined behaviour. With the address sanitizer it reports an alloc-dealloc-mismatch. Always pair new with delete, and new[] with delete[].
The fifth mistake is a memory leak in an early return. This code is correct on the normal path and wrong on the other:
int* build(int n) {
int* data = new int[n];
if (n <= 0) {
return nullptr;
}
return data;
}When n is 0 or negative, the function returns nullptr and the array it allocated is lost. The fix is to check before you allocate. Any function with several return paths needs care that each path frees what it created.
Debugging walkthrough: finding a leak
A program's memory use grows each time you run a loop, and you suspect a leak. Here is a plan that works.
Step 1: build with the sanitizer, g++ -std=c++17 -g -fsanitize=address prog.cpp -o prog. Run it. The leak report names the line of each new that was never freed. Start there.
Step 2: for the named line, follow the pointer. Where does it go after the new? Is it returned, stored in a structure, or only in a local variable? If it is a local that goes out of scope, you have found the leak.
Step 3: count the owners. Each block should have exactly one owner that deletes it. If two places hold the pointer and each thinks the other will delete it, you have a double free or a leak waiting to happen.
Step 4: check every return path. Put a print before each return in the function that allocates, and make sure each one frees the block or hands ownership to the caller.
Step 5: run the program again and check that the report is clean. A clean report is the goal, not a program that merely works.
A case study: a function with two exits
Here is a realistic kind of bug. A function reads numbers into a heap array and returns early when a read fails. The version below looks correct at first glance.
int* readValues(int n) {
int* values = new int[n];
for (int i = 0; i < n; i++) {
if (!(cin >> values[i])) {
return nullptr;
}
}
return values;
}Walk through the two exits. When every read succeeds, the function returns values, and the caller owns the block. When a read fails, the function returns nullptr, and the array is lost. Nothing frees it, and the caller cannot free it, because the caller never received its address. This is the leak pattern from exercise 5, in a more realistic form.
The fix is to free the block on the failure path before returning:
if (!(cin >> values[i])) {
delete[] values;
return nullptr;
}Test both exits. Give the function valid input and check that the caller can free the block. Then give it bad input and check that the sanitizer reports no leak. A function is correct only when every exit has been tested, because bugs on rare paths are the ones that survive review.
Reading a leak report, line by line
The leak report from the mistakes section can look intimidating. Read it in this order.
The first line is a separator. It marks the start of the sanitizer report, so you can tell where the program's own output ends.
The second line says what kind of error it is. LeakSanitizer: detected memory leaks means the program allocated memory and never freed it. The number after the double equals sign is the process id, and it changes on every run, so ignore it.
The third part gives the size and count of the leak. Direct leak of 40 byte(s) in 1 object(s) says that the program lost one block of 40 bytes. Forty bytes is ten ints, which matches new int[10]. Use the size to guess what was allocated. A size you did not expect points at a different allocation.
The last lines are the stack trace of the allocation. Frame #0 is the runtime function that did the allocating, and it is almost never the line to fix. The first frame in your own code, here main at leak.cpp:3, is the line that called new. Go there, find the pointer, and follow it. Ask where it should have been freed, and why the free did not happen.
Some reports list several leaks, and each one starts with the same kind of line. Read them one at a time. Fix the first, run the sanitizer again, and the next report will be shorter or gone.
Variations and edge cases
The malloc and free functions from C also allocate heap memory. C++ code should use new and delete, because new calls constructors and delete calls destructors. Mixing malloc with delete, or new with free, is undefined behaviour. You will see both in older code, and the rule is simple: never mix them.
A new that fails throws a std::bad_alloc exception when memory runs out. The new (std::nothrow) int form returns nullptr instead. Most programs let the exception stop them, and interview answers usually mention only the exception.
A zero-length array, new int[0], is legal and returns a valid pointer that you must still delete[]. Do not depend on its value, and never read from it.
Deep recursion uses the stack, not the heap. A recursive function that goes a million calls deep can exhaust the stack and crash with a stack overflow. The fix is usually to rewrite the recursion as a loop with an explicit structure on the heap, which is a common interview follow-up.
Interview-style questions
Interviewers ask about stack and heap to check whether you know where your data lives. A good answer covers three points: the stack holds frames and locals that end with their function, the heap holds memory you ask for with new that lasts until delete, and the choice affects the lifetime of the data and the speed of allocation.
They also ask what a memory leak is. The right answer: a leak is heap memory that the program still allocated but can no longer reach or free. It is a bug because the memory stays used until the program ends, and a long-running program can run out of memory.
A common question asks why you must use delete[] with new[]. The answer: new[] may record the element count and call the destructor of each element, and delete[] knows how to undo that. Plain delete on an array may skip the destructors or free the wrong amount.
Interview follow-ups on memory
After you explain stack and heap, an interviewer often asks one more question.
What is the cost of new in a loop? Answer with the allocation work, the cache effect and the need for a matching delete. Mention that the allocator reuses memory, so the cost is not always large, but it is never zero.
How would you find a leak in a large program? Answer with the sanitizer, the stack trace in its report and the ownership rules. Say that you would find the allocation site, follow the pointer to where it should be freed, and test each exit path.
Why is a dangling pointer dangerous? Answer that the memory may be reused for other data, so the program reads or writes someone else's values, and the bug may show up far from its cause. Undefined behaviour means the compiler may assume the bad access never happens.
What is the difference between delete and delete[]? Answer that new[] may record the element count and call the destructor of each element, and only delete[] knows how to undo that.
Give short answers with one concrete example each. Interviewers listen for the rule and the reason, and one sentence on what the bug looks like in practice shows that you have met it.
Real-world uses
Servers allocate buffers on the heap for each incoming request, and free them when the response is sent. Game engines allocate a block of memory per level and free it when the level ends. Databases keep their caches on the heap so the cache can grow and shrink as the data changes. The disciplines in this lesson, one owner per block, free on every path, and clear the pointer after the free, are the same ones professional systems code uses.
Exercise 1. Write a program that allocates one double on the heap with the value 2.5, doubles it through the pointer, prints the result (5), and frees it. Set the pointer to nullptr after the delete.
Solution
double* p = new double(2.5);
*p = *p * 2;
cout << *p << endl;
delete p;
p = nullptr;The output is 5. The new gives the block its first value, the write doubles it, and the delete returns it. The nullptr removes the old address from p.
Exercise 2. Write a program that reads n from input, allocates an int array of size n, fills it with the numbers 1 through n, prints their sum, and frees the array. Test with n equal to 1 and 100.
Solution
int n;
cin >> n;
int* a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = i + 1;
}
long long sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
cout << sum << endl;
delete[] a;With n equal to 1 the output is 1. With n equal to 100 it is 5050, which is the classic sum from 1 to 100. The long long total keeps the answer correct for large n.
Exercise 3. Write a function int* makeArray(int n) that returns a heap array of n zeros. Call it in main, print the length of the zeroed array, and free it. Explain why the function can return the pointer safely.
Solution
int* makeArray(int n) {
return new int[n]();
}The caller prints n and then runs delete[] z; on the returned pointer. The function can return the pointer safely, because the array lives on the heap and not in the function's frame. The parentheses in new int[n]() set every element to zero.
Exercise 4. Write a linked list of three nodes with values 5, 6 and 7 and print them. Free every node. Explain why the loop reads next before the delete.
Solution
Build the list the same way as worked example 3, with new Node(5), new Node(6) and new Node(7), and link them with next. Print the values with a loop, which prints 5 6 7. Then free the nodes in a loop that saves cur->next into a variable before it calls delete cur. The next field lives inside the node. Once the node is deleted, reading cur->next would read freed memory, which is a use after free.
Exercise 5. Find the leak in a function that allocates an int and returns early when its input is negative. Fix it, then explain why freeing on each path matters.
Solution
The leaking version is this:
int* f(int x) {
int* p = new int(x);
if (x < 0) {
return nullptr;
}
return p;
}When x is negative, the function returns nullptr, and the block that p pointed to is lost. The fix is to free it on that path, or to check the input before allocating:
int* f(int x) {
if (x < 0) {
return nullptr;
}
return new int(x);
}Every path must either free the block or hand it to the caller. Otherwise the memory stays allocated, and no code can reach it again.
Exercise 6. Explain why int* p = new int; delete p; cout << *p; is undefined behaviour, and what changes when p = nullptr; is added after the delete.
Solution
After the delete, the block belongs to the heap again, and reading it is undefined behaviour. The read may print a leftover value, print garbage or crash. Adding p = nullptr; makes the read a null dereference instead. That is still a bug, and it still crashes on most systems. The gain is that the crash happens at the bad line every time, so the mistake is easy to find. Setting the pointer to nullptr never makes an invalid read valid.
Exercise 7. Write the code that allocates an array of ten ints and sets each to zero with new int[10](). Print the first element. Then explain why new int[10] alone would not be safe to read before writing.
Solution
int* a = new int[10]();
cout << a[0] << endl;
delete[] a;It prints 0. Without the parentheses, new int[10] leaves each element holding whatever bytes were in that memory. Reading an element before writing it is undefined behaviour, because the value is not defined by the language.
Exercise 8. Write a program that calls a function makeCounter in a loop of ten iterations, uses each heap counter once, and frees each counter. Confirm with the address sanitizer that no leak is reported.
Solution
The output is 0 1 2 3 4 5 6 7 8 9 followed by a space. Compiled with -fsanitize=address and run, the program reports no leak, because every makeCounter result is deleted in the same loop iteration.
Exercise 9. Explain to a friend the difference between a stack frame and a heap block, using the plate and warehouse analogies, and say which one ends when a function returns.
Solution
A stack frame is like a plate on a stack of plates. It is put on top when a function starts and removed when the function returns, so it ends with the function. A heap block is like a shelf rented in a warehouse. You ask for it with new, and it stays yours until you return it with delete, however many functions return in the meantime. Only the frame ends when the function returns.
Exercise 10. Given two pointers int* a = new int[5]; and int* b = a;, explain why delete[] a; followed by delete[] b; is a double free. Write a version that is correct.
Solution
The two pointers hold the same address, so both refer to one block. The first delete[] returns the block. The second one tries to return it again, which is a double free. The correct version deletes the block once and clears both pointers:
int* a = new int[5];
int* b = a;
delete[] a;
a = nullptr;
b = nullptr;Clearing both pointers is not required by the language, but it stops either one from being used or deleted again by accident.
What you now know
- The stack holds frames for function calls and their locals, and everything in a frame ends when the function returns.
- The heap holds memory you ask for with
new, and it lasts until you return it withdelete. new int(42)allocates and initialises oneint, andnew int[n]allocates an array ofnvalues.deletepairs withnew,delete[]pairs withnew[], and each pointer must be freed exactly once.- A leak is a
newwith nodelete, a double free is two deletes of one block, and a use after free reads a block that was already returned. - Setting a pointer to
nullptrafter a delete turns a silent bug into an easy one to find. - The address sanitizer,
-fsanitize=address, reports leaks and bad memory use with the line numbers you need.
Lesson 5 shows a better way to manage heap ownership. Smart pointers hold a heap block and delete it automatically, so you do not have to remember every delete by hand.