C++ Part I · Module 10 · Dynamic Memory and Object Lifetime
new and delete
These two operators are usually introduced as a pair of verbs — one gets memory, the other gives it back — and that description is not so much wrong as missing the half that explains everything else in the module. Each of them does two separate jobs. new obtains raw storage and then constructs an object in it; delete destroys the object and then releases the storage. The two jobs are performed by different machinery, they can fail independently, and delete's pair are new's pair run backwards. This chapter makes those four steps visible rather than describing them, by replacing the function that performs the storage half so that it announces itself, and giving the object a constructor and destructor that do the same — so a single new followed by a single delete prints four lines in a fixed order. With that in place the rest of the chapter is a series of consequences: why an uninitialized dynamic object is more dangerous than an uninitialized local, which the compiler proves by warning about one and saying nothing at all about the other; what happens when the storage half fails, measured both ways; and what a four-byte integer actually costs when you ask for it this way, which turned out to be sixteen bytes.
Intermediate17 min readC++Part IFundamentalsMemory
Part I · Module 10 · Chapter 10.3 · new and delete
1. Storage That Belongs to No Scope
Chapter 10.2 settled what happens to an object whose lifetime is decided by a brace. Chapter 10.1 established the three cases where that is not good enough. What is needed is storage that belongs to no scope at all — that begins when you say so and ends when you say so.
int* p = new int(42); // begins here
// ...
delete p; // ends hereBetween those two lines the object exists, regardless of how many functions are entered and left. That is the entire capability, and the rest of the module is about what it costs.
2. The Syntax, Which Is the Easy Part
T* p = new T; // storage + default-initialize
T* p = new T(); // storage + value-initialize
T* p = new T(args); // storage + construct from args
T* p = new T{args}; // the brace form, per Chapter 9.8
delete p; // destroy + releasenew yields a pointer to the object, never the object itself, which is why Module 8 came first. There is a separate pair of operators for a run of objects — new[] and delete[] — and mixing them with these is a measurable error, which is Chapter 10.4.
That is all the syntax. The interesting part is what those lines do.
3. The Four Steps, Measured
The two halves of new are performed by different things: the storage half calls a replaceable function, operator new, and the object half runs a constructor. Replacing the first and instrumenting the second makes all four steps print.
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <new>
// Intercept the STORAGE step so the two halves of `new` are separately visible.
static int allocCount = 0;
static int freeCount = 0;
void* operator new(std::size_t n)
{
++allocCount;
std::printf(" [1] storage obtained: %zu bytes\n", n);
return std::malloc(n);
}
void operator delete(void* p) noexcept
{
++freeCount;
std::printf(" [4] storage released\n");
std::free(p);
}
void operator delete(void* p, std::size_t) noexcept
{
++freeCount;
std::printf(" [4] storage released (sized form)\n");
std::free(p);
}
struct Probe {
int id;
explicit Probe(int i) : id(i) { std::printf(" [2] constructor ran, id=%d\n", id); }
~Probe() { std::printf(" [3] destructor ran, id=%d\n", id); }
};
int main()
{
std::printf("new Probe(7):\n");
Probe* p = new Probe(7);
std::printf("delete p:\n");
delete p;
std::printf("\nallocations: %d frees: %d\n", allocCount, freeCount);
return 0;
}Measured output:
new Probe(7):
[1] storage obtained: 4 bytes
[2] constructor ran, id=7
delete p:
[3] destructor ran, id=7
[4] storage released
allocations: 1 frees: 1
newobtains storage, then constructs.deletedestroys, then releases.
Four events, two per operator, and delete's two are new's two in reverse order. That is the frozen purpose of this chapter and it is now a measurement rather than a claim.
(Two notes on the instrument. Replacing the global operator new is a real language facility, used here only to make the storage step audible — it is not a technique this chapter is teaching, and the Probe constructor and destructor are borrowed from Module 12 for the same reason. Both are instruments.)
4. Why the Two-Job View Matters
Three things follow from §3 that are hard to see any other way.
The storage step and the object step fail differently. Running out of memory is a storage failure and produces std::bad_alloc (§6). A constructor that throws is an object failure — and in that case the storage obtained in step 1 is released automatically, because the object never came into existence. The language handles that one correctly for you.
delete runs a destructor. This is the half most often forgotten, and Chapter 10.4 measures what it costs when the wrong form skips it. delete is not "give the memory back"; it is "end this object's lifetime, then give the memory back".
A pointer is not an object. The pointer p is an ordinary automatic variable living in a stack frame, 8 bytes on this platform (Chapter 8.1 measured it). The object is elsewhere. delete p ends the object and does nothing whatsoever to p, which still holds the same address afterwards — the fact Chapter 10.7 is built on.
5. Initialization, and an Asymmetry Worth Knowing
int* a = new int; // no initializer
int* b = new int(); // empty parens
int* c = new int(5); // explicit valuenew int -> 0 (0x00000000)
new int() -> 0
new int(5) -> 5The third is obvious and the second is guaranteed: new int() value-initializes, which for int means zero. Chapter 9.8 §3 measured the same rule for a struct's omitted members.
The first is the trap, and the output above is misleading. new int leaves the value indeterminate — Chapter 3.8's term. It read 0 here, and I tried deliberately to make it read something else: fill a block with 0xDEADBEEF, release it, then ask for an int back and read it. Three rounds:
round 0: new int read 0x00000000
round 1: new int read 0x00000000
round 2: new int read 0x00000000So on this allocator it came back zero every time. That is not a guarantee and relying on it is a bug; it is exactly the shape of undefined behaviour Chapter 10.2 §7 warned about, where testing agrees with you and the standard does not.
And here is the asymmetry, which is the thing worth carrying away:
| Code | Diagnostic |
|---|---|
int x; std::printf("%d", x); | warning: variable 'x' is uninitialized when used here [-Wuninitialized] |
int* p = new int; std::printf("%d", *p); | 0 warnings |
Chapter 3.8 measured the first. The second is the same mistake, equally undefined, and the compiler cannot see it — it does not know what p points at or whether anything wrote there.
Dynamic allocation removes the compiler's ability to warn you about uninitialized reads.
That is a theme for the whole module: moving an object off the stack costs you diagnostics. The practical rule is to always initialize — write new int() or new int(5), never bare new int for a built-in type.
6. When the Storage Step Fails
Measured, with the size computed at runtime — a compile-time constant that large is rejected outright with error: array is too large:
static std::size_t absurd() { return static_cast<std::size_t>(-1) / 4; }
try {
char* p = new char[absurd()];
std::printf("succeeded?! %p\n", static_cast<void*>(p));
} catch (const std::bad_alloc& e) {
std::printf("new threw std::bad_alloc: %s\n", e.what());
}
char* q = new (std::nothrow) char[absurd()];
std::printf("new (std::nothrow) returned %s\n", q ? "non-null" : "nullptr");new threw std::bad_alloc: std::bad_alloc
new (std::nothrow) returned nullptrTwo separate facts:
Plain new throws std::bad_alloc. It never returns a null pointer. So this, which you will see in older code and in code translated from C, is dead weight:
int* p = new int;
if (p == nullptr) { /* unreachable with plain new */ }new (std::nothrow) returns nullptr instead of throwing, and that is the form where a null check is correct.
(This section borrows try/catch from Chapter 22.2 — there is no other way to observe a thrown exception. Chapter 10.8 borrows it again, and for a more important reason.)
7. What It Costs
sizeof(int) = 4
three separate `new int` addresses, consecutive gaps: 16 and 16 bytesThree new int allocations came back 16 bytes apart, and when one was leaked the leak tool reported it as 16 total leaked bytes. A 4-byte object in a 16-byte slot: the allocator keeps bookkeeping and rounds up to a minimum block.
So new int costs four times the data, plus allocator time, plus the indirection — which is Chapter 10.1 §6's argument with the numbers attached. A million individually allocated ints is 16 MB holding 4 MB of data. One allocation of a million ints is 4 MB plus one block's overhead, which is why Chapter 10.4 exists and why std::vector is the real answer.
8. The delete Rules
Every new needs exactly one delete. Zero is Chapter 10.6; two is Chapter 10.7.
delete nullptr is well defined and does nothing. Verified by running it — the replaced operator delete from §3 was not called at all. So this guard is unnecessary:
if (p != nullptr) delete p; // the check is redundant
delete p; // identical, and correctdelete does not change the pointer. Measured implication in Chapter 10.7: p still holds the old address afterwards, and nothing marks it invalid.
Setting the pointer to nullptr after delete is a worthwhile habit for a pointer that stays in scope, because it turns a later use-after-free into a null dereference — which Chapter 8.3 measured as a reliable immediate crash rather than Chapter 10.7's silent wrong answer.
delete on a pointer you did not get from new is undefined, and Chapter 10.4 measures one such case aborting the process.
9. What delete Does Not Do
Worth stating explicitly because each is a common misconception:
It does not zero the memory. Chapter 10.7 §3 reads a freed block and finds something in it.
It does not change your pointer, as above.
It does not run for objects you never deleted. Chapter 10.5's trace shows a destructor line that simply never appears.
It is not called when the program ends. There is no sweep at exit. Chapter 10.6's measurement of a leaking loop is the demonstration.
Two facilities adjacent to this chapter, named so you recognise them and deferred: placement new, which performs only the object step in storage you already have, and aligned new, for types with stricter alignment than the default — Chapter 9.8 §6's subject. Both belong to Part II.
10. What to Actually Do
Always initialize. new int() or new int(5), never bare new int — the compiler cannot warn you.
Remember delete runs a destructor. It ends a lifetime; releasing storage is the second half.
Do not null-check plain new. It throws std::bad_alloc; it never returns null.
Use new (std::nothrow) only when you genuinely want a null, and then do check it.
Do not guard delete with a null test. delete nullptr is a well-defined no-op.
Set a pointer to nullptr after deleting it if it remains in scope — it converts a silent bug into a loud one.
Never allocate small objects individually. 4 bytes of data in a 16-byte slot, measured.
Pair every new with exactly one delete — and when you cannot see where the second one is, that is Chapter 10.9's question, not a style preference.
11. Mistakes Worth Making Once
new int and then reading it. Indeterminate, 0 warnings, and it read 0 on every attempt here — which is the trap, not the reassurance.
Assuming new int is zeroed because it looked like it. The standard does not say so.
Null-checking plain new. The branch is unreachable.
Forgetting new (std::nothrow) needs the check. That form really can return null.
if (p) delete p; Redundant.
Thinking delete clears the pointer or the memory. It does neither — Chapters 10.7's two bugs both depend on that.
Expecting cleanup at program exit. There is none; Chapter 10.6 measures the consequence.
Allocating a million ints one at a time. 16 MB for 4 MB of data.
Using delete on something from new[]. Chapter 10.4 measures it aborting the process.
12. Interview Relevance
"What does new do?" The strong answer is two things — obtains storage via operator new, then constructs the object — with delete doing the reverse. Candidates who say only "allocates memory" have given the incomplete answer this chapter is built to correct.
"Does new return null on failure?" No: it throws std::bad_alloc. new (std::nothrow) is the form that returns null. A very common question, and the null-check-after-new pattern is a recognisable sign of C habits.
"Is delete nullptr safe?" Yes, well defined, a no-op — so the guard is redundant.
"Difference between new int and new int()?" The second value-initializes to zero; the first leaves it indeterminate. Adding that the compiler warns about the automatic case and not this one is the answer that stands out.
"What does a dynamically allocated int cost?" Measured here: a 16-byte slot for 4 bytes, plus allocator time and an indirection.
13. What You Should Be Able to Say Now
newdoes two things — obtains storage, then constructs — anddeletedoes both in reverse: destroys, then releases.- Measured as four events in order: storage obtained (4 bytes) → constructor → destructor → storage released, with 1 allocation and 1 free.
- The two halves fail differently, and a throwing constructor releases the storage automatically.
- A pointer is not the object —
delete pdoes nothing top. new T()value-initializes;new Tleaves a built-in type indeterminate.- Measured: it read 0 on all three deliberate attempts — which is not a guarantee.
- Measured asymmetry: the automatic uninitialized read warns
[-Wuninitialized]; the dynamic one gets 0 warnings. - Plain
newthrowsstd::bad_allocand never returns null;new (std::nothrow)returnednullptr. - Measured cost:
sizeof(int)4, consecutive allocations 16 bytes apart, and a leaked one reported as 16 bytes. delete nullptris a well-defined no-op — the replacedoperator deletewas not called.deletedoes not zero memory, change your pointer, or run at program exit.- Correctness of
delete pis a whole-program property, and none of its four preconditions is visible at that line.
14. Exercises
Exercise 1 — four steps. For Probe* p = new Probe(3); delete p; list the four events in order and say which operator performs which.
Exercise 2 — predict. new int, new int(), new int(7), new Probe(1), new Probe() — for each, say whether a constructor runs and what the value is.
Exercise 3 — the asymmetry. Explain why the compiler warns about reading an uninitialized int x; and cannot warn about reading *new int. State the rule it implies.
Exercise 4 — failure handling. Rewrite int* p = new int; if (!p) return false; correctly, both for plain new and for the case where you actually want a null.
Exercise 5 — the cost. You need 100,000 four-byte counters. Give the byte totals for individual allocation versus one allocation, using the measured 16-byte figure.
Exercise 6 — what delete does not do. A colleague writes delete p; then if (p) use(p); believing the guard protects them. Explain what is wrong with two separate facts from this chapter.
Exercise 7 — the four preconditions. For a delete p in the middle of a 500-line function, list the four conditions from the callout and say, for each, where in the program you would have to look to verify it.
15. Summary
newobtains storage then constructs;deletedestroys then releases — measured as four events in exactly that order.- The halves fail independently, and a throwing constructor releases the storage for you.
- A pointer is not the object;
delete pleavespunchanged. new T()value-initializes;new Tdoes not. Measured: it read 0 every time, which is not a promise.- The compiler warns about an uninitialized automatic read and not the dynamic one — allocation costs you diagnostics.
- Plain
newthrowsstd::bad_alloc;new (std::nothrow)returnsnullptr. - Measured: 4 bytes of
intin a 16-byte slot. delete nullptris a no-op;deletedoes not zero memory, reset pointers, or run at exit.delete p's correctness is a property of the whole program, none of it visible at the line — which is the module's real subject.
One object at a time is not the interesting case. Chapter 10.1's first limit was a count that is not known until the program runs, and the answer to it is a run of objects rather than a single one — which is where this chapter's cost measurement becomes an argument rather than a footnote, since a thousand separate sixteen-byte slots for four bytes each is not a solution to anything. C++ has a second pair of operators for that, written one bracket apart from the pair above, and the brackets are not decoration. The next chapter measures the order constructors and destructors run in for a whole run of objects, and then measures precisely what it costs to release such a run with the wrong one of the two forms — a mistake the compiler does warn about, and which on this machine ran exactly one destructor out of three before killing the process.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
