Lesson 3 of 7
new asks for heap memory and returns a pointer to it. delete gives it back.
#include <iostream>
int main() {
int* p = new int(42); // one int on the heap, initialised to 42
std::cout << *p << "\n";
*p = 7;
std::cout << *p << "\n";
delete p; // released
return 0;
}
42
7
For arrays there is a second pair, and they are not interchangeable:
#include <iostream>
int main() {
int count = 4;
int* values = new int[count]; // uninitialised
for (int i = 0; i < count; ++i) {
values[i] = (i + 1) * 10;
}
for (int i = 0; i < count; ++i) {
std::cout << values[i] << " ";
}
std::cout << "\n";
delete[] values; // note the brackets
return 0;
}
10 20 30 40
new[] pairs with delete[]. new pairs with delete. Mixing them is undefined behaviour,
and one of the mismatches usually appears to work, which makes it worse than if it always
crashed.
Note new int[count] leaves the elements uninitialised. new int[count]() zeroes them.
The five ways this goes wrong
Every one of these is a real bug people ship. Read them as the case against the syntax you just learned.
1. You forget delete. The memory is never returned — a leak (lesson 8.4).
2. You delete twice. Undefined behaviour, usually a crash or heap corruption.
3. You use the pointer after deleting. Use-after-free (lesson 8.5).
4. You return early and skip the delete:
void process(bool bail) {
int* data = new int[100];
if (bail) {
return; // leaked: delete[] never runs
}
delete[] data;
}
Every return, break, and continue between the new and the delete is a leak waiting to
be added by whoever edits this next.
5. Something throws in between. If an exception is thrown after the new and before the
delete, the delete is skipped and the memory leaks. This one cannot be fixed by being careful,
because the throw can come from any function you call.
Points 4 and 5 are the important ones: they are not carelessness, they are the structure of manual memory management being wrong.
The same code, correct by construction
#include <iostream>
#include <vector>
void process(bool bail) {
std::vector<int> data(100, 7); // owns its heap memory
if (bail) {
std::cout << "bailed, memory freed anyway\n";
return; // destructor runs
}
std::cout << "finished, " << data.size() << " elements\n";
}
int main() {
process(true);
process(false);
return 0;
}
bailed, memory freed anyway
finished, 100 elements
No new. No delete. No leak on any path, including one that throws. The destructor is not
something you remember to call — it is something the language calls for you.
What to write instead
| Instead of | Write |
|---|---|
| new int[n] / delete[] | std::vector<int> |
| new char[n] for text | std::string |
| new T for one owned object | std::unique_ptr<T> (Module 10) |
| new T shared between owners | std::shared_ptr<T> (Module 10) |
| A pointer that just looks at something | a raw T* or T&, owning nothing |
That last row matters: raw pointers are not banned. What is discouraged is raw pointers that own memory. A raw pointer used as a non-owning view is normal, modern C++.
So why learn new at all?
Three reasons, all practical:
- You will read it. Millions of lines of existing C++ use it, and you cannot maintain code you cannot read.
- It is what the containers do inside.
std::vectorcalls the allocator so you do not have to; knowing what it is doing makes its behaviour predictable rather than magical. - You will occasionally write it — inside a class that owns a resource, when no existing wrapper fits. Module 9's Rule of Three exists for exactly that case.
The guidance is not "never" but "not in ordinary code." If you are writing new in a
function body, there is nearly always a container or smart pointer that does it better.
Try it
Write a function that allocates an array with new[], fills it, prints it, and frees it. Then
add an early return in the middle and identify the leak. Finally rewrite it with
std::vector and confirm the early return is now harmless.
Checkpoint
newallocates on the heap;deletefrees it.new[]pairs withdelete[], never mixed.new int[n]leaves elements uninitialised;new int[n]()zeroes them.- Early returns and thrown exceptions skip the
delete— a structural flaw, not carelessness. std::vector,std::stringand smart pointers free their memory on every exit path.- Raw pointers are fine as non-owning views; what is discouraged is raw pointers that own.
- Learn
newto read existing code and to write resource-owning classes, not for everyday work.
Questions about this lesson
Log in to ask a question.