Two Questions About Every Variable

Lesson 3 ended on a promise: a pointer can outlive the thing it points at. Before you can see how, you have to pull apart two questions that beginners reliably fuse into one. Scope asks where in the source text a name is visible, and it is settled entirely at compile time. Storage duration, also called lifetime, asks when the object exists while the program runs. One question is about names, the other about memory, and they are independent axes.

For every variable you have written so far the two answers happen to coincide, which is exactly what makes them easy to confuse. A local variable or a parameter has automatic storage duration: the object comes into existence when control reaches its declaration and stops existing when the enclosing block ends. Lesson 2 already handed you the mechanism, because the block that ends is usually the function body, and the end of the object is the frame being popped. The name follows the same braces, so both answers turn up together at the same closing brace.

Blocks smaller than a function body count too, and about names the compiler is absolute. The last printf below does not compile, because temp's scope ended with the inner block:

#include <stdio.h>

int main(void)
{
    {
        int temp = 7;

        printf("temp is %d\n", temp);
    }

    printf("temp is %d\n", temp);

    return 0;
}
scope.c: In function 'main':
scope.c:11:28: error: 'temp' undeclared (first use in this function)
   11 |     printf("temp is %d\n", temp);
      |                            ^~~~

Nothing subtle happened there. Outside the braces the name simply does not exist, and asking for it is a build error rather than a bug you ship. Hold on to how firmly that door is shut, because the last section of this lesson opens it with a pointer.

A Local That Outlives the Call

Write static on a local declaration and you change one of the two answers while leaving the other alone. A static local has block scope and static storage duration. The name is still visible only inside the function, exactly as before. The object, however, is created before the program starts running and lives until it ends. It sits in no frame at all, so no return can pop it away.

#include <stdio.h>

void greet(void)
{
    static int calls = 0;

    ++calls;
    printf("hello (call number %d)\n", calls);
}

int main(void)
{
    greet();
    greet();
    greet();

    return 0;
}
hello (call number 1)
hello (call number 2)
hello (call number 3)

Three calls, three different numbers, and greet has no parameters and no way to be told which call it is. There is exactly one calls for the whole run, and each call finds it holding whatever the previous call left there. Read static int calls = 0; with that in mind, because the line does not do what its shape suggests. The = 0 is not an assignment executed on every call. A static-duration object is initialized once, conceptually before main begins, and the initializer is only where you say what the starting value should be. If it ran each time, the output would be three copies of call number 1.

That once-only initialization is also where chapter 1 owes you an answer. Its uninitialized int count; held an indeterminate value and reading it was undefined behaviour, with a parenthetical promising the contrast. Here it is: objects with static storage duration are zero-initialized, objects with automatic storage duration are not. Delete the = 0 above and calls still starts at zero, guaranteed by the standard rather than by luck. This is the fact people over-generalize into "C zeroes your variables", and the generalization is what gets them later, so name the storage duration every time you make the claim. Writing = 0 anyway costs nothing at run time and says out loud what you meant. One thing static does not mean here: on a declaration outside any function the same keyword does something unrelated, hiding that name from other source files, which begins to matter in chapter 6 when a program is built from more than one. Same word, two meanings, nothing in common but the spelling. Throughout this lesson static means static storage duration on a local.

The Pointer That Outlives Its Object

Now put the two axes together and you get the most famous bug in C. A function's local has automatic storage duration, so the object is gone once the frame pops. A pointer holding its address does not go anywhere; it is a variable in the caller's frame, still holding a number, and the number now names memory that belongs to nobody. Such a pointer is said to dangle. The function below is wrong on purpose, and it is wrong in the single most common way:

#include <stdio.h>

int *make_answer(void)
{
    int answer = 42;

    return &answer;
}

int main(void)
{
    int *p = make_answer();

    printf("the answer is %d\n", *p);

    return 0;
}

gcc sees this one coming and says so under -Wall:

dangle.c: In function 'make_answer':
dangle.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr]
    7 |     return &answer;
      |            ^~~~~~~

Run it anyway and this is what the platform actually does, trimmed:

==12==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x0000004006dc bp 0xffffdbcaa650 sp 0xffffdbcaa550 T0)
==12==The signal is caused by a READ memory access.
==12==Hint: address points to the zero page.
    #0 0x0000004006dc in main /tmp/dangle.c:14
SUMMARY: AddressSanitizer: SEGV /tmp/dangle.c:14 in main

That is the null-pointer report from lesson 3, address zero and all, which is peculiar given that the word NULL appears nowhere in the program. The explanation is the one that makes dangling pointers dangerous rather than merely broken. Using the value of a pointer whose object's lifetime has ended is undefined behaviour, so the standard imposes no requirement whatsoever on what make_answer gives back, and gcc at -O2 takes what it is offered. Rather than return the address of a frame that no longer exists, it returns a null pointer. The compiler did not attempt to preserve your broken pointer, and nothing obliged it to.

Do not take "returning a local gives you null" away from that, because it is one compiler at one optimization level. The outcome worth fearing is the opposite one, where the dead frame still happens to contain 42 because nothing has reused those bytes yet, the program prints the right answer, the test passes, and it breaks months later when an unrelated call is added between the two lines. That is why the rule is absolute and does not depend on what you observe: never return the address of an automatic local variable. The warning is the reliable signal here. The run is not.

The fix is never to make the pointer work, it is to stop returning one. Return the value instead, so declaring int make_answer(void) and writing return answer; copies the 42 into the caller before the frame goes. Or use lesson 3's out-parameter and let the caller pass the address of an object that is already alive, which puts the lifetime under the control of the code that can see it. If an object truly must outlive the call that made it, then it needs a storage duration longer than automatic: static supplies one at the price of every call sharing a single copy, and chapter 4's heap exists precisely to give an object a lifetime tied to nothing but your own decision to end it.

Lifetime Can End Before the Function Does

The first section's inner block shut its name away at the closing brace. Its object stopped existing at that same brace, and unlike the name, an address can be carried out. The program below is wrong on purpose, and this time the compiler has nothing to say: p = &temp; is an ordinary assignment, every name is used where it is visible, and the build is silent under -Wall -Wextra.

#include <stdio.h>

int main(void)
{
    int *p = NULL;

    {
        int temp = 7;

        p = &temp;
    }

    printf("after the block, *p is %d\n", *p);

    return 0;
}
==12==ERROR: AddressSanitizer: stack-use-after-scope on address 0xfbffa54f0020 at pc 0x0000004007ac bp 0xffffcc1308e0 sp 0xffffcc1308f8
READ of size 4 at 0xfbffa54f0020 thread T0
    #0 0x0000004007a8 in main /tmp/block.c:13
...
    [32, 36) 'temp' (line 8) <== Memory access at offset 32 is inside this variable
SUMMARY: AddressSanitizer: stack-use-after-scope /tmp/block.c:13 in main

stack-use-after-scope names the mistake precisely, and the report goes on to point at the exact variable and the line that declared it. Notice what did not go wrong. main is still running and its frame is entirely alive, so no hardware objected to that address; AddressSanitizer knows that the slot within the frame belonging to temp became unusable at the closing brace and is watching for reads of it. Compare the two diagnostics this lesson opened and closed with, because they came from the same block in the same shape: reaching for the name is a compile error, and reaching for the object through a pointer builds cleanly and is undefined behaviour. The compiler guards names. Nothing in the language guards lifetimes. So the question to ask of any pointer you keep, pass or return is the one this whole lesson is made of: is the object still alive at the moment I use this?

Key Takeaways

  • Scope (where a name is visible, decided at compile time) and storage duration (when the object exists, at run time) are independent questions. Locals and parameters have automatic storage duration, and for them both answers end at the same closing brace.
  • A static local has block scope and static storage duration: visible only inside the function, but created before main runs and alive until the program ends, so its value persists between calls.
  • A static-duration object is initialized once, not on every call, and is zero-initialized if you give no initializer, whereas an automatic object is not. Always say which storage duration you mean, because that contrast is what makes "C zeroes variables" a costly half-truth. static outside a function is an unrelated meaning of the keyword, covered in chapter 6.
  • A dangling pointer holds the address of an object whose lifetime has ended, and using one is undefined behaviour even when the value appears to survive. Never return the address of an automatic local. gcc warns with function returns address of local variable [-Wreturn-local-addr]; here, at -O2, it hands back a null pointer instead and AddressSanitizer reports SEGV on unknown address 0x000000000000, which is a choice the standard leaves open rather than a behaviour to rely on.
  • Return the value, or take an out-parameter, or give the object a longer storage duration. Chapter 4's heap is how an object gets a lifetime independent of any frame.
  • A lifetime can end long before the function does. A pointer to a variable in an inner block dangles the moment that block closes, and AddressSanitizer reports it as stack-use-after-scope.