C++ Part I · Module 10 · Dynamic Memory and Object Lifetime
Why Dynamic Allocation Exists
Chapter 9.11 ended on a representation that was correct and incomplete: a struct describing a signal, and a comment where the thing holding a fileful of them should have been. The reason that gap could not be closed is the property every type in Module 9 shared — its size was fixed when the program was compiled, which is exactly what made it cheap and exactly what stopped it answering however many are in the file. This module is the machinery that answers it, and the module deliberately teaches that machinery as a problem rather than a goal. So this first chapter does not introduce the operators. It establishes the only honest reason to use them, which is that three specific things cannot be done any other way, and it measures all three rather than asserting them: an array whose length comes from a file is not standard C++ at all, an object that must outlive the function that made it cannot live in that function, and an eight-megabyte local array terminated this machine's process with a segmentation fault where a five-hundred-and-twelve-megabyte dynamic one was fine. The chapter then measures what the heap charges you for that, because a reader who takes the first half as encouragement has drawn the wrong conclusion. The rule at the end is a negative one, and it is the point: if none of the three limits applies to the data in front of you, allocating is a mistake.
Intermediate16 min readC++Part IFundamentalsMemory
Part I · Module 10 · Chapter 10.1 · Why Dynamic Allocation Exists
1. The Gap Module 9 Left
Chapter 9.11 §10 reached a representation it could not finish:
struct Signal {
std::string name;
int width;
Access access;
};
const Signal* find(/* the sequence */, std::string_view wanted);The sequence — however many signals are in the file. Module 9 had no way to express that, and Chapter 9.11's callout named the property responsible:
Every type in Module 9 had its size fixed when the program was compiled.
int[4] is a different type from int[5]. Field is 8 bytes. That is what made them cheap — Chapter 9.1 measured that an array's values are the object, with no indirection — and it is what makes them unable to hold a count decided while the program runs.
This chapter is about why that forces a different mechanism. There are exactly three reasons, and the next three sections measure them.
2. Limit One — The Size Must Be a Constant
Chapter 9.1 said an array's bound is part of its type. Here is what that costs when the number comes from outside the program:
#include <cstdio>
int count_from_file() { return 3; } // stands in for reading a real file
int main()
{
const int n = count_from_file();
int a[n]; // is this legal C++?
a[0] = 1;
std::printf("%d\n", a[0]);
return 0;
}At the standing flags this is not an error:
vla.cpp:5:11: warning: variable length arrays in C++ are a Clang extension
[-Wvla-cxx-extension]
5 | int a[n];
| ^
vla.cpp:5:11: note: initializer of 'n' is not a constant expression
vla.cpp:4:15: note: declared hereIt compiles, and it runs, and it prints 1. Which is the trap: it is a compiler extension, not C++. Compiled with -pedantic-errors, the same line is an error.
The note is the precise diagnosis — initializer of n is not a constant expression. const and constant expression are different claims, and this is where the difference bites. Measured, one declaration at a time:
| Bound | int a[n]; |
|---|---|
const int n = 4; | OK |
constexpr int n = 4; | OK |
static const int n = 4; | OK |
int n = 4; | VLA warning |
const int n = f(); | VLA warning — the case above |
constexpr int n = f(); | error: constexpr variable 'n' must be initialized by a constant expression |
The last row is the honest one. constexpr is the keyword for this is known at compile time, and applying it to a value read from a file states something false, so the compiler rejects it. There is no spelling of "a compile-time constant whose value comes from a file", because the thing is a contradiction.
And one case needs no flag caveat at all, because the extension does not reach it:
struct S { int a[f()]; };error: fields must have a constant size: 'variable length array in structure'
extension will never be supportedWill never be supported — a rare thing for a compiler to say, and it settles the question. A type's size is part of the type, so a type cannot have a runtime-decided size.
3. Limit Two — The Lifetime Must End With the Scope
Chapter 7.2 established that lifetime and scope are different questions, and Chapter 7.3 named automatic storage as the kind that ends at the closing brace. That is normally exactly what you want. Here is when it is not:
int* make()
{
int local = 42;
return &local; // the object dies at this brace
}outlive.cpp:4:13: warning: address of stack memory associated with local
variable 'local' returned [-Wreturn-stack-address]And the behaviour, same source, two optimisation levels:
clang++ -O0 ... && ./outlive -> 42
clang++ -O2 ... && ./outlive -> -273071872At -O0 it prints the right answer. That is the whole problem with this class of bug and the reason Chapter 10.2 is a separate chapter: the version that looks correct in testing is the version that is already broken. Chapter 1.3 §5 called this undefined behaviour and demonstrated it with a program that printed false at -O0 and true at -O2; this is the same phenomenon with a different symptom.
The reference form is the same bug and warns the same way:
outlive_ref.cpp:4:12: warning: reference to stack memory associated with local
variable 'local' returned [-Wreturn-stack-address]So: a function that must hand back an object, rather than a copy of one, cannot put that object in automatic storage. A struct returned by value (Chapter 6.3) covers most of this need and should be your first answer. What it does not cover is an object whose size is not known — which is limit one again — or one that must be shared rather than copied, which is where Chapter 10.9's question begins.
Chapter 10.2 owns this topic properly, with the frames drawn.
4. Limit Three — The Stack Is Small
This limit is the one most often discovered the hard way, because nothing warns about it.
$ ulimit -s
8176Eight megabytes, on this machine, for the whole call stack. Measured, with an automatic array of increasing size:
Automatic char[N] | Result |
|---|---|
| 1024 KB | ok |
| 7000 KB | ok |
| 8000 KB | ok |
| 8192 KB | crash, exit 139 (SIGSEGV) |
And the same request made dynamically:
char* big = new char[512 * 1024 * 1024]; // 512 MB
big[0] = 1;
big[512 * 1024 * 1024 - 1] = 2; // touch both endsallocated 512 MB on the heap and touched both ends: ok
exit=0An 8 MB local array killed the process; a 512 MB dynamic one was fine. That is at least a 64× difference, and the real ratio is larger — I stopped at 512 MB rather than hunt for the ceiling.
Two things to take from this rather than just the numbers:
There is no diagnostic. The 8192 KB version compiled without a single warning and then died at runtime. The compiler knows the array's size and does not know your stack limit, so it cannot help.
The failure is a crash, not an error. Exit 139 is the shell reporting signal 11. Chapter 10.6 will show the dynamic equivalent failing in a completely different and much quieter way.
For engineering work the sizes that trip this are ordinary: a 1024×1024 array of double is 8 MB on its own, and a waveform capture or a memory image is routinely larger.
5. One Thing That Is Not a Reason Yet
There is a fourth motivation for dynamic allocation that belongs to Module 17 and is worth naming so you recognise it when you meet it: storing an object whose exact type is decided at runtime, where a function returns something that might be one of several related types. That needs inheritance and virtual functions, neither of which exists yet in this curriculum.
It is named here because reading a codebase you will see allocation used for that purpose and it will not match any of §2 to §4. It is a real fourth reason. It is not one you need, or should imitate, yet.
6. What the Heap Charges
Sections 2 to 4 are not an advertisement, and a reader who finishes them keen to allocate has taken the wrong lesson. Here is the other side.
Space overhead, measured. sizeof(int) is 4. Three consecutive new int allocations came back 16 bytes apart:
sizeof(int) = 4
three separate `new int` addresses, consecutive gaps: 16 and 16 bytesAnd when one such allocation was leaked, the leak tool reported it as 16 total leaked bytes for a 4-byte object. So a dynamically allocated int costs four times what the int is, because the allocator keeps bookkeeping alongside it and rounds to a minimum block. An array of a million ints allocated one at a time is 16 MB where the data is 4 MB.
Time overhead. An automatic object costs essentially nothing to create — Chapter 10.2 measures the frame step as a single subtraction. A dynamic one calls into the allocator, which has to find a block, record it, and later put it back.
An extra indirection on every access. The pointer has to be read before the object can be reached, which is Chapter 9.3's cache lesson pointed at a new target: the pointer and its target can be far apart, and 9.3 measured a 60× difference between a layout the hardware liked and one it did not.
And three entire chapters of failure modes — Chapters 10.6, 10.7 and 10.8 — none of which can happen to an automatic object at all.
7. So the Rule Is Negative
Do not allocate dynamically unless one of §2, §3 or §4 applies.
Stated as questions to ask about the data, in the spirit of Chapter 9.11:
Is the count known at compile time? If yes, use an array or (from Chapter 19.2) a std::array. Nothing here applies.
Does the object need to outlive the function that creates it? If no, automatic storage is correct and cheaper. If yes, first check whether returning by value (Chapter 6.3) solves it — usually it does.
Is it too big for the stack? Over a megabyte or so, start caring; near eight, it is already broken on this machine.
Is it a fixed set of values that exists for the whole program? Then static storage (Chapter 7.4) is the answer, not allocation.
If you got to the end and the answer to all four was no, the right amount of dynamic allocation is none.
8. What to Actually Do
Reach for automatic storage first. It is cheaper, it cannot leak, and its end is guaranteed.
Use static storage for whole-program data (Chapter 7.4), not allocation.
Return a struct by value (Chapter 6.3) before concluding you need to allocate to hand something back.
Treat a runtime count as the real signal. It is the one limit nothing else addresses.
Watch the megabytes. Anything approaching a megabyte of automatic array deserves a second look; 8 MB is fatal here.
Expect roughly 16 bytes minimum per allocation, so do not allocate small objects one at a time.
Never write int a[n] with a runtime n. It compiles here and it is not C++ — Chapter 19.2's std::vector is the correct answer, and until then this module's.
Read the rest of this module as a problem statement. The operators in Chapter 10.3 are not the recommendation; Chapter 10.9's answer is.
9. Mistakes Worth Making Once
Writing int a[n] with a runtime bound. Compiles with a warning on Clang, is not ISO C++, and -pedantic-errors rejects it.
Believing const means compile-time. It does not — const int n = f(); is const and is not a constant expression. The note says so.
Returning the address of a local. Warns, and prints the right answer at -O0, which is what makes it dangerous.
Putting a large array in a function. 8192 KB crashed with no compile-time warning at all.
Allocating because it feels more flexible. Four costs in §6, three chapters of failure modes, and none of the three limits met.
Allocating many small objects individually. 4 bytes of data in a 16-byte slot, measured.
Assuming the heap is unlimited. It is far larger, not infinite, and Chapter 10.3 measures what failure looks like.
Taking this chapter as encouragement. The rule in §7 is negative on purpose.
10. Interview Relevance
"When would you use dynamic allocation?" The strong answer is the three limits — a size not known at compile time, a lifetime that must exceed the creating scope, an object too large for the stack — followed immediately by "and in modern C++ I would reach for vector, string or unique_ptr rather than new". Candidates who answer only with the mechanics of new miss what is being asked.
"What is the difference between the stack and the heap?" Useful concrete anchors: the stack here is 8 MB and automatic objects end at their closing brace with no bookkeeping, while a dynamic int cost 16 bytes and lives until something explicitly releases it. A numerate answer stands out.
"Is int a[n] legal C++?" No — it is a C99 feature that Clang and GCC offer as an extension. Knowing it is an extension, and that it cannot be a struct member at all, is the distinguishing detail.
"Why not just allocate everything dynamically?" Space overhead, allocation time, the extra indirection with its cache consequences, and three categories of bug that automatic storage makes impossible.
11. What You Should Be Able to Say Now
- Every Module 9 type had its size fixed at compile time — the property that made them cheap and made them insufficient.
- Limit one: an array bound must be a constant expression. Measured:
const int n = f(); int a[n];warns as[-Wvla-cxx-extension], errors under-pedantic-errors, and runs when it compiles. constand constant expression are different claims — the compiler's note saysinitializer of 'n' is not a constant expression, andconstexpr int n = f();is a hard error.- A VLA cannot be a struct member at all: "extension will never be supported".
- Limit two: automatic storage ends at the closing brace. Returning
&localwarns[-Wreturn-stack-address]and printed 42 at-O0and -273071872 at-O2. - Limit three: the stack is small. Measured:
ulimit -s8176 KB; an 8192 KB automatic array crashed with exit 139, while 512 MB dynamic was fine. - Neither of the last two is caught at compile time in the way that matters — one warns and still runs, the other does not warn at all.
- A fourth reason — a runtime-decided type — exists and belongs to Module 17.
- The heap charges: ~16 bytes minimum per allocation for a 4-byte
int, allocator time, an extra indirection, and Chapters 10.6–10.8. - The rule is negative: do not allocate unless one of the three limits applies.
- In modern C++ the answer to all three is usually a container or a smart pointer, and this module exists to explain what those are made of.
12. Exercises
Exercise 1 — which limit? For each, say which of the three limits applies, or none: (a) a 16-entry interrupt vector table (b) the lines of a file of unknown length (c) a 4096×4096 array of double (d) a counter local to one function (e) an object a factory function must hand back to its caller.
Exercise 2 — const is not constexpr. Explain why const int n = 4; int a[n]; compiles cleanly but const int n = f(); int a[n]; warns, in terms of the compiler's own note.
Exercise 3 — predict and justify. int size; std::cin >> size; double table[size]; — will it compile at the standing flags? Will it run? Is it C++? Give all three answers with reasons.
Exercise 4 — the dangerous one. A colleague's function returns &local and their tests pass. Explain precisely why the tests pass and why you would still reject it, citing the measurement.
Exercise 5 — size it. Compute the bytes in a local double m[1024][1024] and say what happens on a machine with an 8 MB stack.
Exercise 6 — the overhead. You need one million 4-byte counters. Compare allocating them individually against one allocation, using the measured 16-byte figure.
Exercise 7 — argue the other way. Given §6 and §7, write the case against allocating for a fixed 8-entry table, as you would in a code review.
13. Summary
- Module 9's types all had compile-time sizes — cheap, and unable to express a runtime count.
- Limit one — size: an array bound must be a constant expression.
int a[n]with a runtimenis a Clang extension, warns, runs, and is rejected by-pedantic-errors; as a struct member it "will never be supported". constis notconstexpr— andconstexpr int n = f();is a hard error.- Limit two — lifetime: automatic storage ends at the brace; returning
&localwarned and printed 42 at-O0, garbage at-O2. - Limit three — capacity: stack 8176 KB; 8192 KB automatic crashed (139), 512 MB dynamic was fine.
- A fourth reason, a runtime-decided type, belongs to Module 17.
- The heap charges ~16 bytes minimum per 4-byte
int, allocator time, an indirection, and three chapters of bugs. - The rule is negative: automatic first, static for program-lifetime data, return by value to hand things back, and allocate only when forced.
- Modern C++ answers all three limits with containers and smart pointers — this module explains what they are built from.
The three limits are now established and so is the reason to respect them, but nothing here has explained why automatic storage behaves the way it does. Two of the three limits came from the same place: a local object vanishes at its closing brace, and the space for all of them together is eight megabytes. Both facts follow from one mechanism, and it is a mechanism worth being able to picture rather than recite, because the most dangerous bug in this chapter printed the correct answer at one optimisation level and garbage at another. The next chapter draws it: what a function call actually does to memory, where a local object physically sits, measured addresses from four nested calls showing which direction the whole thing grows, and what is really happening at the instant a function returns a pointer to something inside itself.
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.
