Dataslope logoDataslope

Scope and Modularity

Where names live, and how to split a real program across multiple files

A program of any size eventually becomes too big to fit comfortably in one file. To grow, you need two skills:

  1. Scope — understanding where a name is visible.
  2. Modularity — splitting the program into pieces that hide their internals from each other.

These are the engineering disciplines that turn working code into maintainable code.

What is a scope?

A scope is a region of source code in which a name (a variable, a function) is meaningful. C's scopes are nested like Russian dolls.

int g = 10;                   // file scope (a.k.a. "global")

int main(void) {
    int x = 1;                // function scope: visible inside main only
    {
        int y = 2;            // block scope: visible inside this block only
        printf("%d %d %d\n", g, x, y);
    }
    // y is not visible here
    printf("%d %d\n", g, x);
    return 0;
}

Three levels of scope appear above:

  • File scope — declared outside any function. Visible from the declaration to the end of the file. g above.
  • Function / block scope — declared inside { ... }. Visible only until the matching closing brace. x and y.
  • Function-parameter scope — parameters behave like local variables in the function body.
Code Block
C 17 (201710L)

Shadowing: inner names hide outer names

If an inner scope declares a name that already exists in an outer scope, the inner one shadows the outer one until the inner scope ends.

Code Block
C 17 (201710L)

Shadowing is occasionally useful (e.g. a tiny temporary) but more often a source of bugs. Most real codebases adopt the rule: don't shadow.

Local variables are born and die

A local variable is born when execution enters its scope, and dies when execution leaves. Its memory is the function's stack frame, automatically allocated on entry and reclaimed on return.

You must not keep references (pointers) to a local variable after its function returns. The memory may be reused for the next call. We will hammer this point home in the chapter on pointers.

static: keeping a local across calls

Sometimes you want a local variable that remembers its value across calls — a counter, perhaps. Prefix it with static.

Code Block
C 17 (201710L)

static at file scope means something else: "this name is visible only inside this file". That's the usual way to mark a helper function or global variable as private to the file:

static int helper(int x) { ... }   // not visible to other .c files

Real-world structure: one job per file

A typical small C project looks like:

project/
  main.c          # entry point; ties everything together
  parser.c        # parses input
  parser.h        # public declarations of parser
  evaluator.c     # evaluates parsed structures
  evaluator.h     # public declarations of evaluator
  util.c          # tiny helpers shared by many files
  util.h

Each .c file is a translation unit. The compiler builds each one into a .o. The linker stitches the .os together.

The contract between files is the header. The header lists what the file offers: function signatures, struct definitions, important constants. It says nothing about how those things are implemented.

A worked multi-file example

Below is a tiny "math toolbox" split across three files. Click Run to compile and run all of them at once.

Code Block
C 17 (201710L)

Several rules of the craft are visible here:

  • stats.h is the public interface. Only it is #included by other files.
  • stats.c implements that interface. Nothing else needs to know how.
  • main.c uses the interface without ever seeing the implementation.
  • The header has include guards so it can be #included multiple times safely.

If you later replaced stats.c with a faster implementation, nothing in main.c would need to change — as long as the header contract stayed the same. That is the power of modularity.

A challenge: split a calculator across files

Challenge
C 17 (201710L)
Implement add() and multiply() in calc.c

Open calc.c and implement two functions: int add(int a, int b) and int multiply(int a, int b). main.c and calc.h are already wired up. When the program runs, it should print exactly:

3 + 4 = 7
3 * 4 = 12
QuestionSelect one

What is the most accurate description of static when applied to a variable inside a function?

It makes the variable read-only.

It makes the variable visible to all functions in the file.

It gives the variable a single, persistent storage location that retains its value across calls to the function.

It causes the variable to be initialized every time the function is called.

QuestionSelect one

Why is it a bad idea to return a pointer to a local variable from a function?

The compiler will refuse to build the program.

It would be slower than returning the value directly.

The local variable's memory is reclaimed when the function returns, so the pointer is left pointing at memory that may be overwritten at any time.

C functions cannot return any kind of pointer.

On this page