C Part I · Module 9 · Memory and Object Lifetime
Automatic Objects
Every variable declared inside a function in this entire curriculum has had automatic storage duration, and every one of them has had a lifetime bounded by a pair of braces. That is the default and it is almost always what you want — objects that appear when you need them and disappear when you are done, with no bookkeeping. The difficulty is entirely at the boundary. An address is a value, values can be returned and stored and passed around freely, and nothing stops one from outliving the object it identifies. This chapter works through every shape that mistake takes, which turns out to be more shapes than the compiler warns about, and then gives the alternatives — three of which need nothing you do not already have.
Intermediate18 min readCPart IFundamentalsMemory
Part I · Module 9 · Chapter 9.2 · Automatic Objects
1. The Default You Have Always Been Using
An object declared inside a block, without any storage-class keyword, has automatic storage duration. Its lifetime begins when control enters the block and ends when control leaves it.
That covers every local variable, every parameter, every for control variable and every array declared inside a function that this curriculum has written. The name is the only new thing.
Three properties follow from Chapter 9.1's rule, and all three have already been demonstrated:
A new object per entry. Chapter 9.1 §4's loop printed 1 on every pass because each iteration began a fresh lifetime. Chapter 5.7 §4's three calls printed 5 each time for the same reason.
No automatic initialisation. Entering the block reserves storage; it does not put anything in it. Chapter 2.3 established that reading such an object before assigning to it is a bug, and Chapter 9.12 explains exactly what it holds and why the answer is worse than "a random number".
A valid address — while it lasts. &local is a perfectly good pointer during the object's lifetime. The whole content of this chapter is what happens when it is used afterwards.
2. Four Ways to Get It Wrong
Each of these returns, or hands back, the address of an automatic object whose lifetime has ended.
int *direct(void)
{
int x = 1;
return &x;
}
char *array_version(void)
{
char buf[8] = "text";
return buf; /* Chapter 7.6: converts to &buf[0] */
}
int *via_variable(void)
{
int x = 1;
int *p = &x;
return p;
}
void out_parameter(int **out)
{
int x = 1;
*out = &x; /* Chapter 7.10's pattern, misused */
}All four are the same bug. The caller receives an address, the object at that address stopped existing when the function's block exited, and every subsequent use of that pointer is undefined behaviour.
Compiled with every warning this curriculum has recommended:
warning: address of stack memory associated with local variable 'x' returned
[-Wreturn-stack-address]
warning: address of stack memory associated with local variable 'buf' returned
[-Wreturn-stack-address]
2 warnings generated.Two warnings for four bugs.
| Shape | Caught by -Wall? |
|---|---|
return &x; | yes |
return buf; (an array) | yes |
int *p = &x; return p; | no |
*out = &x; (out-parameter) | no |
One extra local variable between the & and the return, and the diagnostic disappears. The analysis that produces it is deliberately simple and local; it is not a guarantee that your program has no such bug, and it was never intended as one.
The fourth shape is worth particular attention because it is the one that looks most like correct code. Chapter 7.10 §3 established *out = ... as the legitimate way to set a caller's pointer — and it is, provided what you store is an address that will still be valid. Here it is not.
3. What Happens When You Use One
#include <stdio.h>
int *via_variable(void)
{
int x = 41;
int *p = &x;
return p;
}
int main(void)
{
int *dead = via_variable();
printf("%d\n", *dead);
return 0;
}Built and run normally, this program compiles without a single diagnostic, runs to completion, and prints a plausible-looking number.
This chapter will not tell you which number, because it is undefined behaviour and quoting it would present one accident as a result. What matters is the shape of the outcome, not the digits: the program did not crash, did not complain, and produced output that a casual reader would accept.
4. Why It Is Always Wrong
It is worth being precise about the word always, because people reasonably ask whether a small object, or one returned immediately, might be safe.
It is not, for two independent reasons.
The guarantee is gone. Chapter 9.1's rule is unconditional — after the lifetime ends, use of a pointer to the object is undefined. There is no size below which this stops applying and no interval short enough to be safe.
The storage is genuinely reused. The next function call will use the same storage for its own objects, which is exactly what Chapter 5.7's frame model predicts. A value that survives one printf will not survive two, and the failure arrives at whatever point something else happens to write there — typically far from the function that returned the pointer.
A pointer to an automatic object is valid only while that object's block is executing. Returning one is not a risk to be weighed; it is a defect.
5. What to Do Instead
Three options need nothing you have not already met, and they cover the great majority of cases.
#include <stdio.h>
#include <stddef.h>
/* 1 — return the value, not an address */
int doubled(int n)
{
return n * 2;
}
/* 2 — the caller supplies the storage */
void format_label(char *out, size_t capacity, int index)
{
snprintf(out, capacity, "ch-%02d", index);
}
/* 3 — return a pointer the caller already owned */
int *largest(int *values, size_t count)
{
if (count == 0u) {
return NULL;
}
int *best = &values[0];
for (size_t i = 1; i < count; i++) {
if (values[i] > *best) {
best = &values[i];
}
}
return best;
}
int main(void)
{
printf("by value %d\n", doubled(21));
char label[16];
format_label(label, sizeof label, 7);
printf("caller's buffer [%s]\n", label);
int readings[4] = {3, 9, 4, 1};
int *top = largest(readings, 4);
printf("caller's array %d at index %td\n", *top, top - readings);
return 0;
}Output:
by value 42
caller's buffer [ch-07]
caller's array 9 at index 1Return the value. If the result fits in a return value, there is no lifetime question at all. Chapter 5.4 established this as the first choice and it still is.
Let the caller supply the storage. format_label writes into an array that belongs to main, so its lifetime is the caller's problem and the caller already solved it. This is Chapter 7.5's out-parameter and Chapter 7.7's pointer-plus-capacity pairing, with Chapter 8.5's snprintf discipline. It is the standard answer for anything producing text or filling a buffer.
Return a pointer the caller already owned. largest returns a pointer into the caller's array. Nothing about its lifetime is the function's business — it was valid on the way in and is still valid on the way out. This is Chapter 7.10 §3's select_largest, and the key property is that the function did not create the object.
A fourth option exists, which is to obtain storage whose lifetime you control explicitly. That is Chapter 9.6's, and it is the right answer when none of the three above fits — when the size is not known until the program runs, or when the result must outlive both the function and the caller's current block.
There is also a fifth, which works and is usually a bad idea: give the local static storage duration so that it outlives the call. Chapter 9.3 covers what that actually means and why the consequences make it a poor default.
6. Automatic Objects and Recursion
Chapter 5.8 established that each recursive call has its own parameters and locals. In this chapter's vocabulary that is not a special rule: each call enters the function's block, and each entry begins a new lifetime for the automatic objects declared there.
That is also why the depth question in Chapter 5.8 §4 mattered. Every call in progress has its own live automatic objects, all of them existing simultaneously, and that is what the finite region Chapter 5.7 described is holding.
It follows that a pointer to an automatic object in a recursive call is valid only while that particular call is still executing — not merely while the function is somewhere on the call chain.
7. What You Should Be Able to Say Now
- A local declared with no storage-class keyword has automatic storage duration — the default.
- Its lifetime runs from block entry to block exit; re-entering begins a new object.
- Entering a block reserves storage without initialising it — Chapter 9.12 explains what it holds.
&localis a valid pointer only during that lifetime.- Four shapes return one anyway: direct, an array, laundered through a variable, and through an out-parameter.
-Wallcatches only the first two. One extra local, and the warning is gone.- The broken program compiles, runs and prints something plausible.
- AddressSanitizer is also silent by default;
detect_stack_use_after_return=1reportsstack-use-after-return. - It is always wrong — no size is small enough and no interval short enough.
- Three fixes need nothing new: return the value, let the caller supply the storage, return a pointer the caller already owned. Chapter 9.6 adds a fourth.
8. Exercises
Exercise 1 — classify the returns. For each, say whether the returned pointer is usable, and whether -Wall would warn.
int *a(void) { int v = 1; return &v; }
int *b(void) { int v = 1; int *q = &v; return q; }
int *c(int *given) { return given; }
const char *d(void) { return "text"; }
char *e(void) { char s[4] = "ab"; return s; }Exercise 2 — count the objects. For this function called twice, say how many distinct total objects exist over the program's run, and how many exist simultaneously.
int sum_to(int n)
{
int total = 0;
for (int i = 0; i <= n; i++) { int step = i; total += step; }
return total;
}Exercise 3 — find the silent one. Of the four shapes in §2, two produce no diagnostic. Name them, say what they have in common, and explain why a simple local analysis cannot catch them.
Exercise 4 — repair three functions. For each, give a corrected version using one of §5's three approaches, and say which one you chose and why.
char *make_label(int n) { char buf[16]; snprintf(buf, sizeof buf, "n%d", n); return buf; }
int *pick_bigger(int a, int b) { int r = (a > b) ? a : b; return &r; }
void get_first(int *arr, int **out) { int first = arr[0]; *out = &first; }Exercise 5 — explain the three silences. A colleague's program has §3's bug, builds cleanly, passes its tests, and fails in production. Walk through the compiler, the plain run and the default sanitizer build, saying why each one failed to reveal it, and give the one change that would have.
Exercise 6 — justify "always". Someone argues that returning &x is fine if the caller uses it immediately on the next line. Give both reasons from §4, and say what would eventually break the program.
Exercise 7 — reason about recursion. A recursive function takes &local and passes it down to the next call. Say whether that is valid, under what condition, and at exactly what moment it would stop being valid.
Exercise 8 — choose the mechanism. For each requirement, say which of §5's approaches you would use, or whether it needs Chapter 9.6's.
- compute an average from an array
- fill a caller's 64-byte buffer with a formatted message
- return the address of the smallest element of a caller's array
- return a text message whose length is not known until the program runs
9. Summary
- Automatic storage duration is the default for everything declared inside a block.
- Lifetime is block entry to block exit; each entry is a new object, and storage is not initialised.
&localis valid only during that lifetime.- Four shapes return one anyway;
-Wallcatches two — not the laundered one and not the out-parameter. - The broken program compiles, runs, and looks correct.
- AddressSanitizer needs
detect_stack_use_after_return=1to report it. - It is always wrong: the guarantee is gone, and the storage really is reused.
- Fixes: return the value, caller supplies the storage, return a pointer the caller owned — and Chapter 9.6's fourth.
- In recursion, a pointer is valid only while that particular call is executing.
Section 5 mentioned a fifth option in passing and immediately discouraged it: give a local object a lifetime that lasts the whole program, so that returning a pointer to it is safe. That does work, it is one keyword, and it is one of the most misused features in the language — because the keyword also means something entirely unrelated in a different position, and because a lifetime that never ends brings its own problems. The next chapter covers what that storage duration actually gives you, including the one guarantee it provides that automatic objects conspicuously do not.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 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.
