C++ Part I · Module 10 · Dynamic Memory and Object Lifetime
Dynamic Arrays: new[] and delete[]
A single dynamically allocated object is rarely what the problem actually calls for. Chapter 10.1's first limit was a count that is not known until the program runs, and the thing that answers it is a run of objects rather than one — which also rescues the cost measurement from the previous chapter, since a thousand separate sixteen-byte slots holding four bytes each solves nothing. C++ spells the run forms one bracket apart from the single forms, and the brackets carry real meaning: the array versions have to construct every element on the way up and destroy every element on the way down, and they have to know how many there are in order to do it. This chapter measures that order, and the measurement is more convincing than the usual diagram because the element addresses are printed alongside — so you can see the constructors walking up through memory and the destructors walking back down. Then it measures what happens when the two pairs are crossed, which is where the chapter earns its place. The two possible mistakes are not equivalent: one of them is caught by the compiler, aborts the process on this machine, and skips two thirds of the destructors it owed, while the other is caught by the compiler, prints a cheerful message, and exits zero even under a sanitizer. The second is the dangerous one, and the reason why is the subject of the rest of the module.
Intermediate17 min readC++Part IFundamentalsMemory
Part I · Module 10 · Chapter 10.4 · Dynamic Arrays: new[] and delete[]
1. The Form
T* p = new T[n]; // n objects, n constructor calls
delete[] p; // n destructor calls, then one releasen may be a runtime value — that is the whole point, and it is Chapter 10.1's first limit answered. Chapter 10.1 §2 measured that int a[n] with a runtime n is a Clang extension rather than C++; new int[n] is the standard way to say the same thing.
Two rules that the rest of the chapter is about:
new[]must be released bydelete[], andnewbydelete. The forms are not interchangeable.
2. The Run Is Contiguous
#include <cstdio>
struct Probe {
int id;
Probe() : id(0) { std::printf(" ctor %p\n", static_cast<void*>(this)); }
~Probe() { std::printf(" dtor %p\n", static_cast<void*>(this)); }
};
int main()
{
std::printf("new Probe[3]:\n");
Probe* a = new Probe[3];
std::printf(" element addresses: %p %p %p (stride %td)\n",
static_cast<void*>(&a[0]), static_cast<void*>(&a[1]),
static_cast<void*>(&a[2]),
reinterpret_cast<char*>(&a[1]) - reinterpret_cast<char*>(&a[0]));
std::printf("delete[] a:\n");
delete[] a;
return 0;
}Measured:
new Probe[3]:
ctor 0x1028a59f0
ctor 0x1028a59f4
ctor 0x1028a59f8
element addresses: 0x1028a59f0 0x1028a59f4 0x1028a59f8 (stride 4)
delete[] a:
dtor 0x1028a59f8
dtor 0x1028a59f4
dtor 0x1028a59f0Three facts in one output.
The elements are contiguous, stride 4 — exactly Chapter 9.1's measurement for a built-in array, which is why a[i] and pointer arithmetic (Chapter 8.4) work identically here.
Constructors run forward — …9f0, …9f4, …9f8, ascending.
Destructors run backward — …9f8, …9f4, …9f0, descending. The addresses prove it rather than a claim about it.
3. Why Destruction Is Reversed
The reversal is not arbitrary and it is the same rule Chapter 10.2 §2 gave for stack frames: things are destroyed in the reverse order of their construction. Chapter 10.5 will show the identical ordering for automatic objects in a scope.
The reason is dependency. If element 2 was constructed after element 1, element 2 is the one allowed to depend on element 1 existing — so element 2 must go first. For a Probe holding an int this is invisible; for objects that register themselves somewhere, or hold references to each other, reverse order is the only safe choice. It costs nothing to guarantee it always, so the language does.
4. This Is Also the Fix for Chapter 10.3's Cost
Chapter 10.3 §7 measured three separate new int allocations landing 16 bytes apart — a 4-byte object in a 16-byte slot. For a thousand values allocated individually that is 16,000 bytes holding 4,000 bytes of data.
new int[1000] is one allocation: 4,000 bytes of elements plus one block's overhead. The per-object cost disappears, because there is only one object as far as the allocator is concerned.
But the run brings Chapter 9.2's problem with it, undiminished:
int* p = new int[n];p is a pointer, and a pointer carries no length. Chapter 9.2 measured sizeof dropping from 16 to 8 when an array was passed to a function; here there was never a length in the type at all. So:
- you must carry
nyourself, separately, and Chapter 9.2 §5 measured that nothing checks it —sum_with_length(delays, 400)on a four-element array produced zero diagnostics; delete[] pnonetheless destroys exactlynobjects, because the runtime recorded the count next to the block when it was allocated. You do not have access to that number;delete[]does.
That asymmetry is worth sitting with. The information exists, the implementation uses it, and the language does not expose it to you. Which is one more reason the real answer is Chapter 19.2's std::vector: it keeps the count where you can read it.
5. The Mismatch, Direction One: delete on new[]
#include <cstdio>
struct Probe {
int id;
Probe() : id(0) { std::printf(" ctor\n"); }
~Probe() { std::printf(" dtor\n"); }
};
int main()
{
std::setvbuf(stdout, nullptr, _IONBF, 0); // unbuffered — see below
Probe* a = new Probe[3];
std::printf("releasing 3 objects with plain `delete`:\n");
delete a; // WRONG: should be delete[]
std::printf("survived\n");
return 0;
}The compiler catches this one:
mismatch.cpp:15:5: warning: 'delete' applied to a pointer that was allocated
with 'new[]'; did you mean 'delete[]'? [-Wmismatched-new-delete]A good warning — it names the mistake and the fix. And the runtime cost:
ctor
ctor
ctor
releasing 3 objects with plain `delete`:
dtor
exit=133Exactly one destructor ran out of three, and then the process died. Exit 133 is 128 + 5, which is SIGTRAP: the system allocator detected that the pointer handed to it was not the start of a block it had issued, and aborted.
Under AddressSanitizer:
==73040==ERROR: AddressSanitizer: attempting free on address which was not
malloc()-ed: 0x603000001c70 in thread T0
SUMMARY: AddressSanitizer: bad-free mismatch.cpp:13 in main"Not malloc()-ed" is the mechanism: new[] stored the element count just before the elements, so the pointer you hold is not the pointer the allocator issued. delete[] knows to step back; delete does not.
For comparison, the corrected version — delete[] a — gave 3 constructors, 3 destructors, 0 warnings.
A method note worth more than the result. My first run of this program reported no output at all and I almost wrote that down. The output was buffered, the abort discarded the buffer, and the program looked like it had printed nothing. setvbuf(stdout, nullptr, _IONBF, 0) is in the listing for that reason. Never conclude "it printed nothing" from a process that was killed — conclude that you have not seen its output yet.
6. The Mismatch, Direction Two: delete[] on new
int* p = new int(1);
delete[] p; // WRONG the other way
std::printf("survived\n");| Check | Result |
|---|---|
| compiler | 1 warning |
| plain run | printed survived, exit 0 |
| under ASan | printed survived — no report |
It survived, silently, even under the sanitizer.
7. The Asymmetry Is the Lesson
Put the two directions side by side:
| Mistake | Compiler | Runtime | ASan |
|---|---|---|---|
delete on new[] | warns | 1 of 3 dtors, then SIGTRAP | bad-free |
delete[] on new | warns | "survived", exit 0 | nothing |
Both are undefined behaviour. One announces itself loudly; the other does not announce itself at all.
And the quiet one is the dangerous one, for exactly the reason Chapter 10.2 §7 gave: a bug that crashes gets fixed on the day it is written, and a bug that exits zero ships. If you only ever saw direction two, you would reasonably conclude the brackets are cosmetic.
Three things to take from this:
The compiler warning is the reliable detector here, and it catches both directions. This is unusual for Module 10 — Chapters 10.6, 10.7 and 10.8 all measure bugs with zero warnings — so treat -Wmismatched-new-delete as an error and be glad it exists.
"It ran fine" is not evidence about undefined behaviour. It was not fine; it was unobserved.
A sanitizer's silence is not a clean bill of health. ASan found direction one immediately and said nothing about direction two. Chapter 10.2 §8 made the same point for a different reason — there, the relevant check was not even enabled by default.
8. What to Actually Do
Match the forms exactly. new[] with delete[], new with delete. There is no case where crossing them is correct.
Treat -Wmismatched-new-delete as an error. It catches both directions and it is one of very few warnings in this module that catches anything.
Keep n with p. Nothing checks it — Chapter 9.2 measured the consequence.
Decide the release form where the allocation is visible. A function receiving a bare pointer cannot know.
Never free a new[] run with free, or a malloc block with delete[]. Same class of error, no warning across that boundary.
Prefer one allocation to many. Measured in Chapter 10.3: 16 bytes per object individually, versus one block's overhead for the run.
Use std::vector for this in real code (Chapter 19.2). It keeps the count and chooses the release form.
Do not conclude a program is correct because it exited zero — direction two did exactly that, under a sanitizer.
9. Mistakes Worth Making Once
delete on a new[] run. Measured: 1 of 3 destructors, then SIGTRAP (exit 133).
delete[] on a single new. Measured: "survived", exit 0, no ASan report — the quiet one.
Believing the brackets are cosmetic, which direction two would reasonably teach you.
Losing n. The count exists but only delete[] can read it.
Passing a bare pointer and expecting the callee to release it correctly. The type carries neither the count nor the provenance.
Reading "no output" from a crashed process as "it printed nothing." The buffer was discarded; I made this mistake.
Trusting a silent sanitizer. It was silent on direction two.
Allocating a run one element at a time. 16 bytes per 4-byte element.
10. Interview Relevance
"What happens if you use delete instead of delete[]?" Undefined behaviour — and the strong answer is concrete: destructors for all but the first element are skipped, and on a typical implementation the allocator detects a pointer it did not issue and aborts. The measured "1 of 3 destructors, then SIGTRAP" is a much better answer than "it's undefined".
"Why does delete[] know how many to destroy when you don't?" Because the count was recorded next to the block at allocation. A good follow-up is that this is exactly the information a std::vector exposes and a raw pointer does not.
"In what order are array elements destroyed?" Reverse of construction, for the same dependency reason automatic objects are destroyed in reverse.
"How would you design against this class of bug?" Tie the release to a destructor so the correct form is chosen by the only code that knows it — which is the RAII answer, and std::vector is the ready-made one.
11. What You Should Be Able to Say Now
new T[n]takes a runtimen— Chapter 10.1's first limit answered, whereint a[n]was only a compiler extension.- Measured: elements are contiguous, stride 4, as in Chapter 9.1.
- Constructors run forward, destructors backward — proved by ascending then descending addresses.
- Reverse destruction is a dependency rule, the same one Chapter 10.2 gave for frames.
- One allocation replaces N, removing Chapter 10.3's 16-bytes-per-object overhead.
- A
new[]pointer carries no length — Chapter 9.2's problem, undiminished. delete[]knows the count because the runtime stored it; your code cannot read it.- Measured,
deleteonnew[]: warning[-Wmismatched-new-delete], 1 of 3 destructors, exit 133 (SIGTRAP), ASanbad-free. - Measured,
delete[]onnew: warning, "survived", exit 0, no ASan report. - The two directions are not symmetric, and the silent one is the dangerous one.
- The compiler catches both only when it can see both ends; across a function boundary it cannot.
- A pointer is an allocation that forgot how it was made — the type is identical for five different provenances.
- Output buffered before an abort is lost — never read that as "printed nothing".
12. Exercises
Exercise 1 — order. For new Probe[4] followed by delete[], write the eight events in order, and state the rule that fixes the destruction order.
Exercise 2 — predict. Say what happens for each: (a) new int[3] then delete[] (b) new int[3] then delete (c) new int then delete[] (d) new int then delete (e) new int[3] then nothing.
Exercise 3 — the count. Explain how delete[] destroys the right number of objects when your code does not know it, and what that implies about the pointer you hold.
Exercise 4 — unknowable. Given void release(int* p) { delete[] p; }, list the provenances of p that would make this correct and the ones that would make it wrong, and say why no warning is possible.
Exercise 5 — the asymmetry. A colleague argues the brackets do not matter because their code "works". Identify which direction they must have tested and give them the other measurement.
Exercise 6 — cost. Compare new int[100000] with 100,000 separate new int calls, in bytes, using Chapter 10.3's measured figure.
Exercise 7 — design it out. Sketch the struct implied by the callout's two rules — pointer plus count, destructor releasing correctly — and name the standard type it approximates.
13. Summary
new T[n]accepts a runtimen, which is Chapter 10.1's first limit answered properly.- Measured: contiguous with stride 4; constructors forward, destructors reverse, shown by the addresses.
- Reverse destruction is a dependency guarantee, not a convention.
- One allocation removes the 16-byte-per-object overhead Chapter 10.3 measured.
- The pointer carries no length;
delete[]reads a count you cannot. deleteonnew[]: 1 of 3 destructors then SIGTRAP, with a compiler warning and ASanbad-free.delete[]onnew: "survived", exit 0, silent under ASan — the dangerous direction.-Wmismatched-new-deletecatches both, but only where both ends are visible.- A pointer is also an allocation that forgot how it was made.
- Use
std::vector— it keeps the count and owns the release decision.
Everything so far has been described in the language of memory: frames reserved and released, blocks obtained and given back, counts stored beside elements. That vocabulary is useful and it is not what the standard actually talks about, and the difference starts to matter now. The next chapter replaces the picture with the governing idea — an object's lifetime, which begins when construction completes and ends when destruction begins, with storage being a separate and lesser question. That reframing is what makes the two remaining failure modes precisely describable rather than merely bad: a leak is a lifetime that never ended, and a use-after-free is an access to an object whose lifetime already did. It also produces the module's most quietly alarming trace, in which a destructor line that should appear simply does not.
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.
