Functions and the call stack · about 50 min

Scope, lifetime, globals and static

Why this matters

Every variable has two properties that are easy to confuse: where in the source its name can be used (its scope) and when its memory exists while the program runs (its lifetime, or storage duration). Getting them straight explains why a variable declared inside a loop "forgets" its value, why two functions can both have a variable called i without interfering, how a function can remember something between calls, and why experienced C programmers are wary of global variables. It also tells you exactly which region of memory each variable lives in.

Scope: where a name is visible

Scope is a compile-time idea: it is about the text of your program.

Block scope. A name declared inside a block { ... } (including a function body) is visible from its declaration to the closing brace of that block. Function parameters have the scope of the function body. A variable declared in a for header is visible only in that loop.

int main(void)
{
    int a = 1;              /* visible until the end of main */
    if (a > 0) {
        int b = 2;          /* visible only inside this if block */
        printf("%d %d\n", a, b);
    }
    /* b does not exist here: using it gives "error: 'b' undeclared" */
    for (int i = 0; i < 3; i++) {
        /* i visible here */
    }
    /* and not here */
    return 0;
}

File scope. A name declared outside every function is visible from its declaration to the end of the file. Functions themselves have file scope, which is why a function defined above main can be called from main.

Shadowing. An inner block may declare a name that already exists in an outer scope. Inside the inner block, the new name hides the outer one:

int x = 1;
{
    int x = 2;              /* a different variable that shadows the outer x */
    printf("inner x = %d\n", x);
}
printf("outer x = %d\n", x);
inner x = 2
outer x = 1

Shadowing is legal and -Wall -Wextra do not mention it, but it is a common source of bugs (see the quiz). The extra flag -Wshadow reports it:

z.c:10:13: warning: declaration of 'total' shadows a previous local [-Wshadow]
z.c:8:9: note: shadowed declaration is here

Lifetime: when the memory exists

Lifetime is a run-time idea. C has three storage durations you will use (a fourth, thread storage, is for multi-threaded programs):

  process memory

  +----------------------+
  | stack                |  automatic variables: one frame per active call
  |   tick(): b          |
  |   main(): x          |
  +----------------------+
  |        ...           |
  +----------------------+
  | heap                 |  allocated (module 8)
  +----------------------+
  | data / bss           |  static storage: globals and static locals
  |   total_calls, calls |  exist for the whole run
  +----------------------+
  | code                 |
  +----------------------+

Initialization differs too

Automatic variables are not initialized unless you initialize them. They start with whatever bytes were left in that part of the stack by earlier calls: garbage. Reading one before assigning it is undefined behavior. gcc catches the simple cases:

u2.c:5:5: warning: 'x' is used uninitialized [-Wuninitialized]

but not all of them: int sum; for (...) sum += i; produces no warning at all at -O0, and only gets one when compiling with optimization (-O2), because the deeper analysis runs as part of the optimizer. Make it a habit to initialize every variable when you declare it.

Static-storage variables are always initialized, exactly once, before main runs. If you give no initializer, they start as zero. The initializer must be a constant expression (you cannot write static int n = some_function(); in C). Zero-initialized statics go into a section called .bss, which takes no space in the executable file; the kernel simply provides zeroed memory.

static local variables: memory that survives calls

Putting static on a local variable keeps its scope local (only that function can name it) but gives it static lifetime: one variable for the whole program, kept between calls.

#include <stdio.h>

int total_calls = 0;          /* global: file scope, static storage */
int never_set;                /* global with no initializer: starts at 0 */

int count_calls(void)
{
    static int calls = 0;     /* initialized once, keeps its value */
    int temp = 0;             /* automatic: fresh on every call */
    calls++;
    temp++;
    total_calls++;
    printf("calls = %d, temp = %d\n", calls, temp);
    return calls;
}

int main(void)
{
    count_calls();
    count_calls();
    count_calls();
    printf("total_calls = %d, never_set = %d\n", total_calls, never_set);
    return 0;
}
calls = 1, temp = 1
calls = 2, temp = 1
calls = 3, temp = 1
total_calls = 3, never_set = 0

The line static int calls = 0; is not an assignment executed on each call; it describes the initial value the variable has when the program starts. That surprises people coming from other languages.

Static locals are useful for counters, ID generators, caches of an expensive result, and "initialize on first use" flags. Their downside: the function now has hidden state, so calling it twice with the same arguments can give different results, which makes it harder to test and unsafe to call from several threads at once.

Global variables

A variable declared at file scope is often called a global. Any function in the file can read and change it (and, as module 11 shows, functions in other files too). That convenience is exactly the problem:

Reasonable uses exist: a program-wide configuration flag set once at startup, or a table of constants. But the default should be: pass data in through parameters and out through return values, and when a function truly needs private memory between calls, use a static local, which at least limits who can touch it.

You can see where the compiler put each variable with nm on the object file:

0000000000000008 b calls.0
0000000000000000 T count_calls
0000000000000058 T main
0000000000000004 B never_set
                 U printf
0000000000000000 B total_calls

B is a global in .bss; b (lowercase) is a symbol private to this file, here the static local calls, renamed calls.0 so it cannot clash with other functions' statics. T marks functions. The exercises in this lesson use exactly this output to check that you used a static local rather than a global.

static on functions and globals: a preview

The keyword static has a second meaning at file scope. A function or global declared static is private to its file: other source files cannot see or call it. This is called internal linkage:

static int helper(int x)      /* usable only within this .c file */
{
    return x * 2;
}

In a single-file program this changes nothing visible, but in multi-file programs (module 11) it is how C does encapsulation: everything not meant to be called from outside is static.

So static means two different things depending on where it appears:

whereeffect
inside a function, on a variablestatic lifetime (survives calls), scope still local
at file scope, on a variable or functioninternal linkage: private to this file

Common mistakes

Recap

Going further: compile a program with a few globals, static locals and functions with gcc -c, then run nm and size on the object file. Initialize one global to a nonzero value and watch it move from B (bss) to D (initialized data).

Exercises

Exercise 1 · quiz

Predict the counts

What does this program print?

#include <stdio.h>

void tick(void)
{
    static int a = 10;
    int b = 10;
    a++;
    b++;
    printf("%d %d\n", a, b);
}

int main(void)
{
    tick();
    tick();
    tick();
    return 0;
}
Answer
  • 11 11, 12 11, 13 11

static int a = 10; is initialized once, before the program starts, and keeps its value between calls: it lives in the data region, not in tick's stack frame. int b = 10; is an automatic variable: a new b is created and set to 10 on every call, and discarded when tick returns. So a counts 11, 12, 13 while b is 11 every time.

Exercise 2 · quiz

Which total?

What does this print?

int total = 0;
for (int i = 1; i <= 3; i++) {
    int total = 0;
    total += i;
}
printf("%d\n", total);
Answer
  • 0

The int total inside the loop body is a new variable in an inner block. It shadows (hides) the outer total within that block, so total += i updates the inner one, which is recreated as 0 on each pass and thrown away at the closing brace. The outer total is never touched. Declaring the same name in a nested block is legal, which is why -Wall does not complain; the extra flag -Wshadow reports "declaration of 'total' shadows a previous local".

Exercise 3 · write code

An ID generator

Write a function

int next_id(void)

that returns 1 the first time it is called, 2 the second time, 3 the third, and so on. Keep the counter inside the function as a static local variable: the check rejects a global variable (it inspects the compiled object file for global data symbols) and calls next_id five times in a row.

File: main.c

Starting point

#include <stdio.h>

int next_id(void)
{
    int id = 0;
    id++;
    return id;
}

int main(void)
{
    printf("%d\n", next_id());
    printf("%d\n", next_id());
    printf("%d\n", next_id());
    return 0;
}

Example

expected output

1
2
3
Hint 1

Run the starter: it prints 1 three times, because id is a fresh automatic variable on each call.

Hint 2

Adding one keyword to the declaration of id fixes it: static int id = 0;

Reference solution
#include <stdio.h>

int next_id(void)
{
    static int id = 0;
    id++;
    return id;
}

int main(void)
{
    printf("%d\n", next_id());
    printf("%d\n", next_id());
    printf("%d\n", next_id());
    return 0;
}

Exercise 4 · write code

A running average

Write a function

double add_sample(double x)

that records one more sample and returns the average of all samples so far. It must remember the count and the sum between calls using static local variables (no globals).

Then write main to read numbers with scanf("%lf", ...) until the input ends and, after each one, print the running average with two decimals. For input 4 8 6 2 the output is:

4.00
6.00
6.00
5.00

The check also calls add_sample directly.

File: main.c

Starting point

#include <stdio.h>

double add_sample(double x)
{
    return x;
}

int main(void)
{
    double x;
    while (scanf("%lf", &x) == 1)
        printf("%.2f\n", add_sample(x));
    return 0;
}

Example

input

4 8 6 2

expected output

4.00
6.00
6.00
5.00

Example

input

3.5

expected output

3.50
Hint 1

You need two pieces of memory that survive between calls: the running sum and the count.

Hint 2

static double sum = 0.0; static int count = 0; then add x to sum, increment count, return sum / count.

Reference solution
#include <stdio.h>

double add_sample(double x)
{
    static double sum = 0.0;
    static int count = 0;
    sum += x;
    count++;
    return sum / count;
}

int main(void)
{
    double x;
    while (scanf("%lf", &x) == 1)
        printf("%.2f\n", add_sample(x));
    return 0;
}