Proven C Book한국어 GitHub

44 Lifetime and storage duration

What to know first

chapter 24, Declaring and defining functions · while a function runs
chapter 2, The regions of memory · the stack and the static region

Looking back

Chapter 24 taught scope — the range in which a name is visible. But chapter 43 used the phrase “the return ledger of a function call”. Are a name ceasing to be visible and that memory vanishing the same thing?

A. They are different, and that distinction is this chapter’s skeleton. Scope is a translation-time notion (where that name may be used) and lifetime is a run-time notion (from when to when that memory is valid). Usually they travel together, but the moment they diverge is an accident — the name has gone and the address remains (see the misconception box below).

The need for this chapter, and its context

The stack and static regions drawn in chapter 2 meet C’s syntax here. Placing this right before chapter 45 (dynamic memory) is the point of the order: knowing what automatic lifetime cannot do is what makes malloc read as a necessity rather than a convenience. One accident — keeping the address of something gone — is shown first, here.

By the end of this chapter

We draw the layout of memory — where a variable lives, when it is born and when it dies. Automatic and static lifetime, the ledger called the stack, and the accident to be most careful of in this part (keeping the address of something that has vanished).

The questions this chapter answers

  1. Then what about const static int x;? Two words together.
  2. What is that strange name bss? And why is it separated from data?
  3. Are global variables — declared outside functions — of static lifetime too?

44.1 The standard’s four axes — storage duration, scope, linkage, storage class

Let us set the terms out formally here. The C standard defines four mutually independent properties for names and objects, and lumping them together guarantees confusion later (especially over static’s two faces).

standard termwhat it fixesa notion of when
storage durationfrom when to when the object existsrun time
scopewhere that name may be usedtranslation time
linkagewhether it is the same thing as that name elsewheretranslation and link time
storage-class specifierhow the three above are writtensyntax

Table 44.1

The last line matters. static, extern, auto, register, typedef, and C11′s _Thread_local (C23′s thread_local) are words occupying one slot syntactically, and which of them you write fixes the three properties above. There is only one such slot per declaration, so static extern int x; is a syntax error.

44.1.1 Storage duration — four

storage durationborn when, dies whenhow it is made
automaticon entering a block   on leaving itan ordinary declaration inside a block
staticbefore the program starts   when it endsa file-scope declaration, or static
threadwhen that thread starts   when it endsthread_local (C11)
allocatedmalloc   freechapter 46

Table 44.2

The standard’s word is not dynamic but allocated storage duration. What is commonly called “dynamic allocation” is this, and this book follows the common usage while recording the standard term here.

44.1.2 Scope — four

The range in which a name is visible. The C standard divides it into four.

scopevisible how farexample
blockfrom the declaration to the end of that blocka local variable inside a function
filefrom the declaration to the end of that translation unita declaration outside functions
functionthe whole of that functionlabel names only (the targets of goto)
function prototypeinside the prototype’s parenthesesparameter names written in a prototype

Table 44.3

The third will look unfamiliar: it is the special case that a label is visible throughout the function wherever inside it it is written. The fourth fixes how far a name written only in a prototype, as in void f(int count);, lives — it disappears outside the parentheses, so a prototype’s parameter names are effectively comments.

The inner hides the outer. When the same name overlaps, the inner block’s wins.

int n = 1;                  /* file scope */
void f(void) {
    int n = 2;              /* block scope — hides the outer n */
    { int n = 3; use(n); }  /* here it is 3 */
    use(n);                 /* here it is 2 */
}

44.1.3 Linkage — three

It fixes whether the same name appearing in several places refers to one and the same object. It is the groundwork of chapter 54 (several files).

linkagemeaninghow it comes about
externalthe same thing across translation unitsthe default at file scope, extern
internalthe same thing only within this translation unitstatic at file scope
noneseparate for each declarationordinary variables in a block, parameters, typedef names

Table 44.4

Counter-example. Reading static’s two faces as the same thing

The same word does entirely different jobs depending on where it is written. Seen through the standard’s terms there is no room for confusion.

#idx("internal")  static int counter;        /* file scope: makes the linkage *internal* (the duration was static anyway) */

  void f(void) {
      static int calls;      /* block scope: makes the storage duration *static* (there is no linkage) */
  }

The first static does not change the lifetime — a file-scope variable has static storage duration regardless. What it changes is the linkage, and it means “this name cannot be used outside this file.” The second static has nothing to do with linkage — a local name has none to begin with. What it changes is the storage duration.

Memorise it in one sentence: static at file scope hides; static at block scope keeps alive.

44.1.4 The seven storage-class specifiers — gathered in one place

The words that may fill this slot number seven in C23, and that is all of them. This is the place that gathers the list.

examples-en/ch44/storage_class.c

/* See what a storage-class specifier decides.
   Also the rule that one declaration takes only one of them - and its
   single exception. */
#include <stdio.h>

static int file_only = 1;          /* internal linkage - visible only in this file */
extern int shared;                 /* a promise only - the real thing is below */
int        shared = 2;             /* the definition */
thread_local int per_thread = 3;   /* one per thread */

static int bump(void)
{
    static int calls = 0;          /* static storage duration - survives between calls */
    return ++calls;
}

int main(void)
{
    auto int local = 4;            /* the pre-C23 meaning: automatic storage duration (the default) */
    register int hot = 5;          /* the old request. its address cannot be taken */
    constexpr int fixed = 6;       /* C23 - a compile-time constant */
    static_assert(fixed == 6, "constexpr is a constant expression");

    printf("file_only=%d shared=%d per_thread=%d local=%d hot=%d fixed=%d\n",
           file_only, shared, per_thread, local, hot, fixed);
    /* Calling it several times in one statement leaves the order unspecified
       (chapter 34), so call it once per line. */
    int b1 = bump(), b2 = bump(), b3 = bump();
    printf("bump() three times: %d %d %d   <- a static local survives\n", b1, b2, b3);

    /* static extern int bad;   <- error: multiple storage classes            */
    /* int *p = &hot;           <- error: address of register variable        */
    /* auto constexpr int c=1;  <- error: 'auto' used with 'constexpr'        */
    static thread_local int ok = 7;   /* the one combination allowed as an exception */
    printf("static thread_local ok=%d\n", ok);
    return 0;
}

Output

file_only=1 shared=2 per_thread=3 local=4 hot=5 fixed=6
bump() three times: 1 2 3   <- a static local survives
static thread_local ok=7
WordStorage durationLinkageWhat it does
staticstaticinternal at file scope★ two faces — inside a function it fixes lifetime, at file scope linkage
externstaticexternal“defined somewhere else”. takes no memory
autoautomaticnonethe old meaning stated the default. ★ C23 reused the slot for type inference
registerautomaticnonean old request. ★ the one effect left is its address cannot be taken
thread_localthreadas writtenan object per thread (chapter 81)
constexpras writtenas writtenC23. a constant whose value is fixed at compile time
typedef★ the same syntactic slot, an entirely different job — it makes a type name (chapter 60)

Table 44.5

The example shows all seven at once. The four starred rows are worth unfolding.

The two faces of static are what the previous section showed. The same word fixes a lifetime in one context and a linkage in another — C’s most famous reuse of a word.

auto is a word whose meaning changed. Originally it stated “automatic storage duration”, which nobody wrote because it was the default. C23 gave the empty slot to type inference — write auto x = 1 + 2; and the type comes from the initialiser. The old use, auto int local;, still compiles (the example writes it that way), but there is no reason to put it in new code.

register is almost dead, but not entirely. The request “keep this in a register if you can” has no effect on today’s optimisers — register allocation is something the compiler does far better (chapter 11). ★ But one rule survived: the address of an object declared register cannot be taken. The example’s comment marks the spot; compile it and it says address of register variable 'hot' requested. Some people use this deliberately, to pin down for the compiler and the reader that “this variable’s address never escapes”.

typedef is here because of the grammar. It occupies the same slot as a storage class but makes no object. So read it like this — “in the place where this declaration would create a variable, a type name is created instead” (chapter 60).

44.1.5 The slot holds one — and its exception

★ A declaration may take only one storage-class specifier. Measured, it says this:

static extern int x;      -> error: multiple storage classes in declaration specifiers
typedef static int T;     -> error: multiple storage classes in declaration specifiers
auto constexpr int k = 1; -> error: 'auto' used with 'constexpr'

The exception is thread_local, alone. It may be written together with static or extern, because thread storage duration and linkage are separate axes and both sometimes need fixing. The example’s static thread_local int ok = 7; is that case.

Q. Then what about const static int x;? Two words together.

A. It is fine. And the question is a good test for telling the two families apart. const is not a storage class but a type qualifier (chapter 26), so it occupies a different slot to begin with. That is why stacking one from each family, as in static const volatile unsigned long x;, is all legal.

What you cannot do is take two from one familystatic extern is that. The order is free (const static and static const are the same), but the convention is to write the storage class first.

44.2 Two lifetimes

A local variable in C has, by default, automatic storage duration — born on entering a block, dead on leaving. That parameters and local variables are born anew on each function call was the ground on which chapter 33′s recursion stood.

Attach static and it becomes static storage duration — born once before the program starts and living until it ends (initialised exactly once too). The demonstration contrasts the two.

examples-en/ch44/life.c

#include <stdio.h>

int next_ticket(void)
{
    static int issued = 0;      /* static lifetime: it survives between calls */
    issued += 1;
    return issued;
}

int fresh_count(void)
{
    int n = 0;                  /* automatic lifetime: born anew on every call */
    n += 1;
    return n;
}

int main(void)
{
    /* calls with side effects get their own statements — chapter 29's rule as it is */
    int t1 = next_ticket();
    int t2 = next_ticket();
    int t3 = next_ticket();
    printf("next_ticket: %d %d %d\n", t1, t2, t3);

    int f1 = fresh_count();
    int f2 = fresh_count();
    int f3 = fresh_count();
    printf("fresh_count: %d %d %d\n", f1, f2, f3);
    return 0;
}

Output

next_ticket: 1 2 3
fresh_count: 1 1 1

next_ticket’s issued keeps its value between calls and grows 1, 2, 3, while fresh_count’s n is born anew on each call and is always 1. Both are “variables inside a function” and yet their lifetimes differ. (Note in addition that the demonstration split the calls into separate statements — exactly chapter 33′s rule that piling side-effecting calls into one expression leaves the evaluation order unspecified.)

44.3 The layout of memory — the regions with our own eyes

At this point let us see in one picture how a program’s memory is actually laid out. Below are addresses printed directly on this book’s verification machine.

examples-en/ch44/regions.c

/* Where a program's memory really sits, confirmed by addresses.
   (The concrete values differ by machine, operating system and run. What to
    look at is the *order of the regions*.) */
#include <stdio.h>
#include <stdlib.h>

const char  ro_text[]  = "read-only";    /* a constant — usually the read-only region */
int         initialized = 7;             /* a global with a value — data */
int         zeroed;                      /* a global without one — bss   */

static void deeper(int depth, char *outer)
{
    char here;                            /* a local variable of this frame */
    if (depth == 0) {
        printf("  local variable one frame deeper : %p\n", (void *)&here);
        printf("  difference from the outer frame : %+ld bytes\n",
               (long)(&here - outer));
        printf("  => the stack grows toward %s addresses\n",
               (&here < outer) ? "lower" : "higher");
        return;
    }
    deeper(depth - 1, outer);
}

int main(void)
{
    static int  static_local;             /* local, but with static storage duration */
    int         automatic = 1;            /* automatic storage duration — the stack */
    void       *heap1 = malloc(64);
    void       *heap2 = malloc(64);

    printf("code (the function main)   : %p\n", (void *)(void (*)(void))main);
    printf("read-only string           : %p\n", (void *)ro_text);
    printf("global (with a value, data): %p\n", (void *)&initialized);
    printf("global (without one, bss)  : %p\n", (void *)&zeroed);
    printf("static inside a function   : %p\n", (void *)&static_local);
    printf("heap (malloc 1)            : %p\n", heap1);
    printf("heap (malloc 2)            : %p\n", heap2);
    printf("stack (a local variable)   : %p\n", (void *)&automatic);
    printf("\ngap between the two heap blocks : %+ld bytes\n", (long)((char *)heap2 - (char *)heap1));
    printf("distance from stack to heap     : about %.1f TiB (a 64-bit address space is this wide)\n\n",
           ((double)((char *)&automatic - (char *)heap1)) / (1024.0 * 1024.0 * 1024.0 * 1024.0));

    char anchor;                          /* the reference point for measuring the stack's direction */
    deeper(1, &anchor);

    free(heap1);
    free(heap2);
    return 0;
}

Output

code (the function main)   : 0x55c08e4151f1
read-only string           : 0x55c08e416008
global (with a value, data): 0x55c08e418028
global (without one, bss)  : 0x55c08e418030
static inside a function   : 0x55c08e418034
heap (malloc 1)            : 0x55c0b66e62a0
heap (malloc 2)            : 0x55c0b66e62f0
stack (a local variable)   : 0x7ffe8ab42aec

gap between the two heap blocks : +80 bytes
distance from stack to heap     : about 42.2 TiB (a 64-bit address space is this wide)

  local variable one frame deeper : 0x7ffe8ab42a9f
  difference from the outer frame : -76 bytes
  => the stack grows toward lower addresses

The way to read it is the order, not the values. From low addresses they line up like this.

regionwhat lives therelifetime
codethe machine instructions of functionsthe whole program (read-only)
read-only datastring literals, const datathe whole program (writing collapses)
dataglobals and statics with an initial valuethe whole program
bssglobals and statics with no (= zero) initial valuethe whole program
heapwhat malloc gave (chapter 46)until freed
stacklocal variables, parameters, return addressesuntil the function ends

Table 44.6

The example confirmed three things. data and bss sit side by side, the heap grows upward above them, and the stack grows downward from a far distant high address (the example’s last three lines stack one more frame and measure that direction).

Q. What is that strange name bss? And why is it separated from data?

A. The name is an abbreviation of a 1950s assembler instruction, Block Started by Symbol — the meaning was forgotten and only the name crossed half a century.

The reason for separating them is practical. Consider a large global whose initial value is 0, such as int table[1000000];. Put it in data and a million zeros go inside the executable, making the file 4 MB bigger. Put it in bss and only one number saying “fill this much with zeros” is written in the file, with the actual filling happening when the program starts. So variables in bss get C’s promise that “an uninitialised one is 0” for free — that zero was filled in by the operating system (or, in embedded work, the startup code).

Platform note. How large is the stack — Linux and Windows

The C standard has neither the word “stack” nor any promise about its size. It fixes only automatic storage duration and leaves where and how to place it to the implementation. So the size is decided by the operating system and the tools.

  • Linux — the main thread’s default limit is usually 8 MiB (check and change it with ulimit -s; this book’s verification machine was 8388608 bytes too). It can be raised if needed, and the stack made for each thread is set separately with pthread_attr_setstacksize.
  • Windows — the default is 1 MiB. Moreover only the first 4 KiB of it is actually committed, growing as it is used. Change it with the linker option /STACK:reserve[,commit] when building the executable, and for a thread specify it as an argument to CreateThread.

There is a place where the difference shows in practice. Code that ran fine on Linux dying of stack overflow on Windows — the same code, a container eight times narrower. A large local array (char buf[2*1024*1024];) or deep recursion are the candidates. The fuller layout, and the circumstances of embedded work, are treated in chapter 83.

44.4 The stack — the ledger of calls

The place where automatic variables live has a name — the stack. Each time a function is called, a bundle of slots for that call (a stack frame) is laid on top, and when the function ends it is lifted off whole. A frame contains, along with local variables and parameters, the address to return to (the return address) — the identity of the “return ledger” whose name was brushed past in chapter 43.

With the picture in place two things are explained at once. First, why chapter 33′s recursion piles up in layers — because one frame is laid on per call. And recurse too deeply and the stack space runs out and the program collapses (stack overflow — the representative symptom of infinite recursion). Second, why chapter 43′s boundary-violation attack is so dangerous — overflow an array on the stack and you can overwrite the return address of that same frame, whereupon the function, on finishing, “returns” to a place the attacker chose. One array’s boundary is connected to control of the program.

A common misconception. “The address of a variable inside a function can still be used after the function ends”

The commonest accident right after learning pointers, and the frightening part is that it appears to work for a while. When a function ends the frame is lifted, but the bits in that place are not immediately erased, so following a dead variable’s address still reads the old value for a time. Then, the moment another function uses that place as its frame, the value flips — becoming a bug that goes off later, somewhere unrelated. Such an address is called a dangling pointer, and the rule is one: the address of a local variable must not outlive its function. To send a function’s result out to live longer, there are three ways: use static lifetime, use the next chapter’s dynamic memory, or fill a container the caller provided (chapter 35′s & idiom). Chapter 17′s ASan is also the representative tool for catching this accident at run time.

Q. Are global variables — declared outside functions — of static lifetime too?

A. They are. A variable outside functions has static storage duration and lives for the whole program. Separately from lifetime, though, a question of visibility attaches — whether to make it visible in several files or keep it to this one is the subject of chapter 54 (linkage). And the practical advice is an old one: keep mutable global state to a minimum. A value that can change anywhere is hard to trace, and in chapter 12′s multicore world it is a source of accidents. A static inside a function has the same property in miniature (the demonstration’s issued), so convenient though it is, the practice is not to overuse it.

Two places in the layout of memory — the stack (automatic) and the static region — are learned. The remaining place is this part’s last: memory whose size is settled at run time and which stays alive as long as you wish — chapter 45′s dynamic memory.